Vous êtes sur la page 1sur 457

APPRENTISSAGE DU LANGAGE C# 2008 et du Framework .NET 3.

5
Serge Tah - ISTIA - Universit d'Angers Mai 2008

Introduction
C# est un langage rcent. Il a t disponible en versions beta successives depuis lanne 2000 avant dtre officiellement disponible en fvrier 2002 en mme temps que la plate-forme .NET 1.0 de Microsoft laquelle il est li. C# ne peut fonctionner quavec cet environnement dexcution. Celui-ci rend disponible aux programmes qui sexcutent en son sein un ensemble trs important de classes. En premire approximation, on peut dire que la plate-forme .NET est un environnement dexcution analogue une machine virtuelle Java. On peut noter cependant deux diffrences : Java s'excute sur diffrents OS (windows, unix, macintosh) depuis ses dbuts. En 2002, la plate-forme .NET ne s'excutait que sur les machines Windows. Depuis quelques annes le projet Mono [http://www.mono-project.com] permet d'utiliser la plate-forme .NET sur des OS tels que Unix et Linux. La version actuelle de Mono (fvrier 2008) supporte .NET 1.1 et des lments de .NET 2.0. la plate-forme .NET permet l'excution de programmes crits en diffrents langages. Il suffit que le compilateur de ceux-ci sache produire du code IL (Intermediate Language), code excut par la machine virtuelle .NET. Toutes les classes de .NET sont disponibles aux langages compatibles .NET ce qui tend gommer les diffrences entre langages dans la mesure o les programmes utilisent largement ces classes. Le choix d'un langage .NET devient affaire de got plus que de performances.

En 2002, C# utilisait la plate-forme .NET 1.0. C# tait alors largement une copie de Java et .NET une bibliothque de classes trs proche de celle de la plate-forme de dveloppement Java. Dans le cadre de l'apprentissage du langage, on passait d'un environnement C# un environnement Java sans tre vraiment dpays. On trouvait mme des outils de conversion de code source d'un langage vers l'autre. Depuis, les choses ont volu. Chaque langage et chaque plate-forme de dveloppement a dsormais ses spcificits. Il n'est plus aussi immdiat de transfrer ses comptences d'un domaine l'autre. C# 3.0 et le framework .NET 3.5 amnent beaucoup de nouveauts. La plus importante est probablement LINQ (Language INtegrated Query) qui permet de requter de faon uniforme, d'une faon proche de celle du langage SQL, des squences d'objets provenant de structures en mmoire telles que les tableaux et les listes, de bases de donnes (SQL Server uniquement pour le moment - fvrier 2008) ou de fichiers XML. Ce document n'est pas un cours exhaustif. Par exemple, LINQ n'y est pas abord. Il est destin des personnes connaissant dj la programmation et qui veulent dcouvrir les bases de C#. Il est une rvision du document originel paru en 2002. Plusieurs livres m'ont aid crire ce cours : Pour la version 2002 : - Professional C# programming, Editions Wrox - C# et .NET, Grard Leblanc, Editions Eyrolles A l'poque j'avais trouv excellents ces deux ouvrages. Depuis Grard Leblanc a publi des versions mises jour dont la suivante : C# et .NET 2005, Grard Leblanc, Editions Eyrolles

Pour la rvision de 2008, j'ai utilis les sources suivantes : le document initial de 2002. Ce document issu d'un copier / coller de mon cours Java comportait la fois des erreurs de typographie et des erreurs plus srieuses telles que dire que les types primitifs comme System.Int32 taient des classes alors que ce sont des structures. Mea culpa... la documentation MSDN de Visual Studio Express 2008 le livre C# 3.0 in a Nutshell de Joseph et Ben Albahari aux ditions O'Reilly, l'un des meilleurs livres de programmation qu'il m'ait t donn de lire. Les codes source des exemples de ce document sont disponibles sur le site [http://tahe.developpez.com/dotnet/csharp/]. Serge Tah, mai 2008

Installation de Visual C# 2008

Fin janvier 2008, les versions Express de Visual Studio 2008 sont tlchargeables [2] l'adresse suivante [1] : [http://msdn2.microsoft.com/fr-fr/express/future/default(en-us).aspx] : 1

[1] : l'adresse de tlchargement [2] : l'onglet des tlchargements [3] : tlcharger C# 2008

L'installation de C# 2008 entranera celle d'autres lments :


le framework .NET 3.5 le SGBD SQL Server Compact 3.5 la documentation MSDN

Pour crer un premier programme avec C# 2008, on pourra procder comme suit, aprs avoir lanc C# :

4b

3 4

4c

[1] : prendre l'option File / New Project [2] : choisir une application de type Console [3] : donner un nom au projet - il va tre chang ci-dessous [4] : valider [4b] : le projet cr [4c] : Program.cs est le programme C# gnr par dfaut dans le projet.

Installation de Visual C# 2008

6 7 10 9 11

la 1re tape n'a pas demand o placer le projet. Si on ne fait rien, il sera sauvegard un endroit par dfaut qui ne nous conviendra probablement pas. L'option [5] permet de sauvegarder le projet dans un dossier prcis. on peut donner un nouveau nom au projet dans [6], prciser dans [7] son dossier. Pour cela on peut utiliser [8]. Ici, le projet sera au final dans le dossier [C:\temp\08-01-31\MyApplication1]. en cochant [9], on peut crer un dossier pour la solution nomme dans [10]. Si Solution1 est le nom de la solution : un dossier [C:\temp\08-01-31\Solution1] sera cr pour la solution Solution1 un dossier [C:\temp\08-01-31\Solution1\MyApplication1] sera cr pour le projet MyApplication1. Cette solution convient bien aux solutions composes de plusieurs projets. Chaque projet aura un sous-dossier dans le dossier de la solution. 1

3 2

en [1] : le dossier windows du projet MyApplication1 en [2] : son contenu en [3] : le projet dans l'explorateur de projets de Visual Studio

Modifions le code du fichier [Program.cs] [3] comme suit :


1. 2. 3. 4. 5. 6. 7. 8. 9. using System; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Console.WriteLine("1er essai avec C# 2008"); } } }

ligne 3 : l'espace de noms de la classe dfinie ligne 4. Ainsi le nom complet de la classe, dfinie ligne 4, est-il ici ConsoleApplication1.Program. lignes 5-7 : la mthode statique Main qui est excute lorsqu'on demande l'excution d'une classe ligne 6 : un affichage cran

Le programme peut tre excut de la faon suivante :

Installation de Visual C# 2008

1. 2.

1er essai avec C# 2008 Appuyez sur une touche pour continuer...

2 1

[Ctrl-F5] pour excuter le projet, en [1] en [2], l'affichage console obtenu.

L'excution a rajout des fichiers au projet :

3 2

en [1], on fait afficher tous les fichiers du projet en [2] : le dossier [Release] contient l'excutable [MyApplication1.exe] du projet. en [3] : le dossier [Debug] qui contiendrait lui aussi un excutable [MyApplication1.exe] du projet si on avait excut le projet en mode [Debug] (touche F5 au lieu de Ctrl-F5). Ce n'est pas le mme excutable que celui obtenu en mode [Release]. Il contient des informations complmentaires permettant au processus de dbogage d'avoir lieu.

On peut rajouter un nouveau projet la solution courante :

2 1

4 3

[1] : clic droit sur la solution (pas le projet) / Add / New Project [2] : choix d'un type d'application [3] : le dossier propos par dfaut est celui contenant le dossier du projet dj existant [MyApplication1] [4] : donner un nom au nouveau projet

La solution a alors deux projets :

Installation de Visual C# 2008

[1] : le nouveau projet [2] : lorsque la solution est excute par (F5 ou Ctrl-F5), l'un des projets est excut. C'est celui qu'on dsigne par [2].

Un projet peut avoir plusieurs classes excutables (contenant une mthode Main). Dans ce cas, on doit prciser la classe excuter lorsque le projet est excut : 3

2 4

[1, 2] : on copie / colle le fichier [Program.cs] [3] : le rsultat du copier / coller [4,5] : on renomme les deux fichiers

La classe P1 (ligne 4) :
1. 2. 3. 4. using System; namespace MyApplication2 { class P1 {

Installation de Visual C# 2008

5. 6. 7. 8.

} }

static void Main(string[] args) { }

La classe P2 (ligne 4) :
1. 2. 3. 4. 5. 6. 7. 8. using System; namespace MyApplication2 { class P2 { static void Main(string[] args) { } } }

Le projet [MyApplication2] a maintenant deux classes ayant une mthode statique Main. Il faut indiquer au projet laquelle excuter :

5 3 2

en [1] : les proprits du projet [MyApplication2] en [2] : le choix de la classe excuter lorsque le projet est excut (F5 ou Ctrl-F5) en [3] : le type d'excutable produit - ici une application Console produira un fichier .exe. en [4] : le nom de l'excutable produit (sans le .exe) en [5] : l'espace de noms par dfaut. C'est celui qui sera gnr dans le code de chaque nouvelle classe ajoute au projet. Il peut alors tre chang directement dans le code, si besoin est.

Installation de Visual C# 2008

1
1.1

Les bases du langage C#


Introduction

Nous traitons C# d'abord comme un langage de programmation classique. Nous aborderons les classes ultrieurement. Dans un programme on trouve deux choses : des donnes les instructions qui les manipulent

On s'efforce gnralement de sparer les donnes des instructions :


+--------------------+ DONNEES +-------------------- INSTRUCTIONS +--------------------+

1.2

Les donnes de C#

C# utilise les types de donnes suivants: 1. 2. 3. 4. 5. 6. les nombres entiers les nombres rels les nombres dcimaux les caractres et chanes de caractres les boolens les objets

1.2.1

Les types de donnes prdfinis


Type .NET Donne reprsente caractre chane de caractres nombre entier .. .. .. .. .. .. .. nombre rel .. nombre dcimal .. rfrence d'objet Suffixe des valeurs littrales Codage Domaine de valeurs

Type C#

char string int uint long ulong sbyte byte short ushort float double decimal bool object

Char (S) String (C) Int32 (S) UInt32 (S) Int64 (S) UInt64 (S) Byte (S) Int16 (S) UInt16 (S) Single (S) Double (S) Decimal (S) Boolean (S) Object (C)

2 octets 4 octets 4 octets 8 octets

caractre Unicode (UTF-16) rfrence sur une squence de caractres Unicode

U L UL

F D M

[-231, 231-1] [2147483648, 2147483647] [0, 232-1] [0, 4294967295] [-263, 263 -1] [9223372036854775808, 9223372036854775807] 8 octets [0, 264 -1] [0, 18446744073709551615] 1 octet [-27 , 27 -1] [-128,+127] 1 octet [0 , 28 -1] [0,255] 2 octets [-215, 215-1] [-32768, 32767] 2 octets [0, 216-1] [0,65535] 4 octets [1.5 10-45, 3.4 10+38] en valeur absolue 8 octets [-1.7 10+308, 1.7 10+308] en valeur absolue 16 octets [1.0 10-28,7.9 10+28] en valeur absolue avec 28 chiffres significatifs 1 octet true, false rfrence d'objet

Ci-dessus, on a mis en face des types C#, leur type .NET quivalent avec le commentaire (S) si ce type est une structure et (C) si le type est une classe. On dcouvre qu'il y a deux types possibles pour un entier sur 32 bits : int et Int32. Le type int est un type C#.

Les bases du langage C#

Int32 est une structure appartenant l'espace de noms System. Son nom complet est ainsi System.Int32. Le type int est un alias C# qui dsigne la structure .NET System.Int32. De mme, le type C# string est un alias pour le type .NET System.String. System.String est une classe et non une structure. Les deux notions sont proches avec cependant la diffrence fondamentale suivante : une variable de type Structure se manipule via sa valeur une variable de type Classe se manipule via son adresse (rfrence en langage objet). Une structure comme une classe sont des types complexes ayant des attributs et des mthodes. Ainsi, on pourra crire :
string nomDuType=3.GetType().FullName;

Ci-dessus le littral 3 est par dfaut de type C# int, donc de type .NET System.Int32. Cette structure a une mthode GetType() qui rend un objet encapsulant les caractristiques du type de donnes System.Int32. Parmi celles-ci, la proprit FullName rend le nom complet du type. On voit donc que le littral 3 est un objet plus complexe qu'il n'y parat premire vue. Voici un programme illustrant ces diffrents points :
1. using System; 2. 3. namespace Chap1 { 4. class P00 { 5. static void Main(string[] args) { 6. // exemple 1 7. int ent = 2; 8. float fl = 10.5F; 9. double d = -4.6; 10. string s = "essai"; 11. uint ui = 5; 12. long l = 1000; 13. ulong ul = 1001; 14. byte octet = 5; 15. short sh = -4; 16. ushort ush = 10; 17. decimal dec = 10.67M; 18. bool b = true; 19. Console.WriteLine("Type de ent[{1}] : [{0},{2}]", ent.GetType().FullName, ent,sizeof(int)); 20. Console.WriteLine("Type de fl[{1}]: [{0},{2}]", fl.GetType().FullName, fl, sizeof(float)); 21. Console.WriteLine("Type de d[{1}] : [{0},{2}]", d.GetType().FullName, d, sizeof(double)); 22. Console.WriteLine("Type de s[{1}] : [{0}]", s.GetType().FullName, s); 23. Console.WriteLine("Type de ui[{1}] : [{0},{2}]", ui.GetType().FullName, ui, sizeof(uint)); 24. Console.WriteLine("Type de l[{1}] : [{0},{2}]", l.GetType().FullName, l, sizeof(long)); 25. Console.WriteLine("Type de ul[{1}] : [{0},{2}]", ul.GetType().FullName, ul, sizeof(ulong)); 26. Console.WriteLine("Type de b[{1}] : [{0},{2}]", octet.GetType().FullName, octet, sizeof(byte)); 27. Console.WriteLine("Type de sh[{1}] : [{0},{2}]", sh.GetType().FullName, sh, sizeof(short)); 28. Console.WriteLine("Type de ush[{1}] : [{0},{2}]", ush.GetType().FullName, ush, sizeof(ushort)); 29. Console.WriteLine("Type de dec[{1}] : [{0},{2}]", dec.GetType().FullName, dec, sizeof(decimal)); 30. Console.WriteLine("Type de b[{1}] : [{0},{2}]", b.GetType().FullName, b, sizeof(bool)); 31. } 32. } 33. }

ligne 7 : dclaration d'un entier ent ligne 19 : le type d'une variable v peut tre obtenue par v.GetType().FullName. La taille d'une structure S peut tre obtenue par sizeof(S). L'instruction Console.WriteLine("... {0} ... {1} ...",param0, param1, ...) crit l'cran, le texte qui est son premier paramtre, en remplaant chaque notation {i} par la valeur de l'expression parami. ligne 22 : le type string dsignant une classe et non une structure, on ne peut utiliser l'oprateur sizeof.

Voici le rsultat de l'excution :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. Type Type Type Type Type Type Type Type Type Type de de de de de de de de de de ent[2] : [System.Int32,4] fl[10,5]: [System.Single,4] d[-4,6] : [System.Double,8] s[essai] : [System.String] ui[5] : [System.UInt32,4] l[1000] : [System.Int64,8] ul[1001] : [System.UInt64,8] b[5] : [System.Byte,1] sh[-4] : [System.Int16,2] ush[10] : [System.UInt16,2]

Les bases du langage C#

11. Type de dec[10,67] : [System.Decimal,16] 12. Type de b[True] : [System.Boolean,1]

L'affichage produit les types .NET et non les alias C#.

1.2.2

Notation des donnes littrales


145, -7, 0xFF (hexadcimal) 100000L 134.789, -45E-18 (-45 10-18) 134.789F, -45E-18F (-45 10-18) 100000M 'A', 'b' "aujourd'hui" "c:\\chap1\\paragraph3" @"c:\chap1\paragraph3" true, false new DateTime(1954,10,13) (an, mois, jour) pour le 13/10/1954

entier int (32 bits) entier long (64 bits) - suffixe L rel double rel float (suffixe F) rel decimal (suffixe M) caractre char chane de caractres string boolen bool date

On notera les deux chanes littrales : "c:\\chap1\\paragraph3" et @"c:\chap1\paragraph3". Dans les chanes littrales, le caractre \ est interprt. Ainsi "\n" reprsente la marque de fin de ligne et non la succession des deux caractres \ et n. Si on voulait cette succession, il faudrait crire "\\n" o la squence \\ est interprte comme un seul caractre \. On pourrait crire aussi @"\n" pour avoir le mme rsultat. La syntaxe @"texte" demande que texte soit pris exactement comme il est crit. On appelle parfois cela une chane verbatim.

1.2.3
1.2.3.1

Dclaration des donnes


Rle des dclarations

Un programme manipule des donnes caractrises par un nom et un type. Ces donnes sont stockes en mmoire. Au moment de la traduction du programme, le compilateur affecte chaque donne un emplacement en mmoire caractris par une adresse et une taille. Il le fait en s'aidant des dclarations faites par le programmeur. Par ailleurs celles-ci permettent au compilateur de dtecter des erreurs de programmation. Ainsi l'opration x=x*2; sera dclare errone si x est une chane de caractres par exemple.

1.2.3.2

Dclaration des constantes

La syntaxe de dclaration d'une constante est la suivante :


const type nom=valeur; //dfinit constante nom=valeur

Par exemple :
const float myPI=3.141592F;

Pourquoi dclarer des constantes ? 1. La lecture du programme sera plus aise si l'on a donn la constante un nom significatif :
const float taux_tva=0.186F;

2.

La modification du programme sera plus aise si la "constante" vient changer. Ainsi dans le cas prcdent, si le taux de tva passe 33%, la seule modification faire sera de modifier l'instruction dfinissant sa valeur :
const float taux_tva=0.33F;

Si l'on avait utilis 0.186 explicitement dans le programme, ce serait alors de nombreuses instructions qu'il faudrait modifier.

1.2.3.3

Dclaration des variables

Une variable est identifie par un nom et se rapporte un type de donnes. C# fait la diffrence entre majuscules et minuscules. Ainsi les variables FIN et fin sont diffrentes.

Les bases du langage C#

10

Les variables peuvent tre initialises lors de leur dclaration. La syntaxe de dclaration d'une ou plusieurs variables est :
Identificateur_de_type variable1[=valeur1],variable2=[valeur2],...;

o Identificateur_de_type est un type prdfini ou bien un type dfini par le programmeur. De faon facultative, une variable peut tre initialise en mme temps que dclare. On peut galement ne pas prciser le type exact d'une variable en utilisant le mot cl var en lieu et place de Identificateur_de_type :
var variable1=valeur1,variable2=valeur2,...;

Le mot cl var ne veut pas dire que les variables n'ont pas un type prcis. La variable variablei a le type de la donne valeuri qui lui est affecte. L'initialisation est ici obligatoire afin que le compilateur puisse en dduire le type de la variable. Voici un exemple :
1. using System; 2. 3. namespace Chap1 { 4. class P00 { 5. static void Main(string[] args) { 6. int i=2; 7. Console.WriteLine("Type de int i=2 : {0},{1}",i.GetType().Name,i.GetType().FullName); 8. var j = 3; 9. Console.WriteLine("Type de var j=3 : {0},{1}", j.GetType().Name, j.GetType().FullName); 10. var aujourdhui = DateTime.Now; 11. Console.WriteLine("Type de var aujourdhui : {0},{1}", aujourdhui.GetType().Name, aujourdhui.GetType().FullName); 12. } 13. } 14. }

ligne 6 : une donne type explicitement ligne 7 : (donne).GetType().Name est le nom court de (donne), (donne).GetType().FullName est le nom complet de (donne) ligne 8 : une donne type implicitement. Parce que 3 est de type int, j sera de type int. ligne 10 : une donne type implicitement. Parce que DateTime.Now est de type DateTime, aujourdhui sera de type DateTime.

A l'excution, on obtient le rsultat suivant :


1. 2. 3. Type de int i=2 : Int32,System.Int32 Type de var j=3 : Int32,System.Int32 Type de var aujourdhui : DateTime,System.DateTime

Une variable type implicitement par le mot cl var ne peut pas ensuite changer de type. Ainsi, on ne pourrait crire aprs la ligne 10 du code, la ligne :
var aujourdhui = "aujourd'hui";

Nous verrons ultrieurement qu'il est possible de dclarer un type " la vole" dans une expression. C'est alors un type anonyme, un type auquel l'utilisateur n'a pas donn de nom. C'est le compilateur qui donnera un nom ce nouveau type. Si une donne de type anonyme doit tre affecte une variable, la seule faon de dclarer celle-ci est d'utiliser le mot cl var.

1.2.4
nombre chaine chane chane chane

Les conversions entre nombres et chanes de caractres


-> -> -> -> -> chane int long double float

nombre.ToString() int.Parse(chaine) ou System.Int32.Parse long.Parse(chaine) ou System.Int64.Parse double.Parse(chane) ou System.Double.Parse(chane) float.Parse(chane) ou System.Float.Parse(chane)

La conversion d'une chane vers un nombre peut chouer si la chane ne reprsente pas un nombre valide. Il y a alors gnration d'une erreur fatale appele exception. Cette erreur peut tre gre par la clause try/catch suivante : try{

Les bases du langage C#

11

appel de la fonction susceptible de gnrer l'exception } catch (Exception e){ traiter l'exception e } instruction suivante Si la fonction ne gnre pas d'exception, on passe alors instruction suivante, sinon on passe dans le corps de la clause catch puis instruction suivante. Nous reviendrons ultrieurement sur la gestion des exceptions. Voici un programme prsentant quelques techniques de conversion entre nombres et chanes de caractres. Dans cet exemple la fonction affiche crit l'cran la valeur de son paramtre. Ainsi affiche(S) crit la valeur de S l'cran o S est de type string.
1. using System; 2. 3. namespace Chap1 { 4. class P01 { 5. static void Main(string[] args) { 6. 7. // donnes 8. const int i = 10; 9. const long l = 100000; 10. const float f = 45.78F; 11. double d = -14.98; 12. 13. // nombre --> chane 14. affiche(i.ToString()); 15. affiche(l.ToString()); 16. affiche(f.ToString()); 17. affiche(d.ToString()); 18. 19. //boolean --> chane 20. const bool b = false; 21. affiche(b.ToString()); 22. 23. // chane --> int 24. int i1; 25. i1 = int.Parse("10"); 26. affiche(i1.ToString()); 27. try { 28. i1 = int.Parse("10.67"); 29. affiche(i1.ToString()); 30. } catch (Exception e) { 31. affiche("Erreur : " + e.Message); 32. } 33. 34. // chane --> long 35. long l1; 36. l1 = long.Parse("100"); 37. affiche(l1.ToString()); 38. try { 39. l1 = long.Parse("10.675"); 40. affiche(l1.ToString()); 41. } catch (Exception e) { 42. affiche("Erreur : " + e.Message); 43. } 44. 45. // chane --> double 46. double d1; 47. d1 = double.Parse("100,87"); 48. affiche(d1.ToString()); 49. try { 50. d1 = double.Parse("abcd"); 51. affiche(d1.ToString()); 52. } catch (Exception e) { 53. affiche("Erreur : " + e.Message); 54. } 55. 56. // chane --> float 57. float f1; 58. f1 = float.Parse("100,87"); 59. affiche(f1.ToString()); 60. try { 61. d1 = float.Parse("abcd"); 62. affiche(f1.ToString()); 63. } catch (Exception e) {

Les bases du langage C#

12

64. affiche("Erreur : " + e.Message); 65. } 66. 67. }// fin main 68. 69. public static void affiche(string S) { 70. Console.Out.WriteLine("S={0}",S); 71. } 72. }// fin classe 73. }

Lignes 30-32, on gre l'ventuelle exception qui peut se produire. e.Message est le message d'erreur li l'exception e. Les rsultats obtenus sont les suivants :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. S=10 S=100000 S=45,78 S=-14,98 S=False S=10 S=Erreur S=100 S=Erreur S=100,87 S=Erreur S=100,87 S=Erreur

: Input string was not in a correct format. : Input string was not in a correct format. : Input string was not in a correct format. : Input string was not in a correct format.

On remarquera que les nombres rels sous forme de chane de caractres doivent utiliser la virgule et non le point dcimal. Ainsi on crira
double d1=10.7;

mais
double d2=int.Parse("10,7");

1.2.5

Les tableaux de donnes

Un tableau C# est un objet permettant de rassembler sous un mme identificateur des donnes de mme type. Sa dclaration est la suivante : Type[] tableau[]=new Type[n] n est le nombre de donnes que peut contenir le tableau. La syntaxe Tableau[i] dsigne la donne n i o i appartient l'intervalle [0,n-1]. Toute rfrence la donne Tableau[i] o i n'appartient pas l'intervalle [0,n-1] provoquera une exception. Un tableau peut tre initialis en mme temps que dclar :
int[] entiers=new int[] {0,10,20,30};

ou plus simplement :
int[] entiers={0,10,20,30};

Les tableaux ont une proprit Length qui est le nombre d'lments du tableau. Un tableau deux dimensions pourra tre dclar comme suit : Type[,] tableau=new Type[n,m]; o n est le nombre de lignes, m le nombre de colonnes. La syntaxe Tableau[i,j] dsigne l'lment j de la ligne i de tableau. Le tableau deux dimensions peut lui aussi tre initialis en mme temps qu'il est dclar :
double[,] rels=new double[,] { {0.5, 1.7}, {8.4, -6}};

ou plus simplement :

Les bases du langage C#

13

double[,] rels={ {0.5, 1.7}, {8.4, -6}};

Le nombre d'lments dans chacune des dimensions peut tre obtenue par la mthode GetLength(i) o i=0 reprsente la dimension correspondant au 1er indice, i=1 la dimension correspondant au 2ime indice, Le nombre total de dimensions est obtenu avec la proprit Rank, le nombre total d'lments avec la proprit Length. Un tableau de tableaux est dclar comme suit : Type[][] tableau=new Type[n][]; La dclaration ci-dessus cre un tableau de n lignes. Chaque lment tableau[i] est une rfrence de tableau une dimension. Ces rfrences tableau[i] ne sont pas initialises lors de la dclaration ci-dessus. Elles ont pour valeur la rfrence null. L'exemple ci-dessous illustre la cration d'un tableau de tableaux :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. // un tableau de tableaux string[][] noms = new string[3][]; for (int i = 0; i < noms.Length; i++) { noms[i] = new string[i + 1]; }//for // initialisation for (int i = 0; i < noms.Length; i++) { for (int j = 0; j < noms[i].Length; j++) { noms[i][j] = "nom" + i + j; }//for j }//for i

ligne 2 : un tableau noms de 3 lments de type string[][]. Chaque lment est un pointeur de tableau (une rfrence d'objet) dont les lments sont de type string[]. lignes 3-5 : les 3 lments du tableau noms sont initialiss. Chacun "pointe" dsormais sur un tableau d'lments de type string[]. noms[i][j] est l'lment j du tableau de type string [] rfrenc par noms[i]. ligne 9 : initialisation de l'lment noms[i][j] l'intrieur d'une double boucle. Ici noms[i] est un tableau de i+1 lments. Comme noms[i] est un tableau, noms[i].Length est son nombre d'lments.

Voici un exemple regroupant les trois types de tableaux que nous venons de prsenter :
12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. using System; namespace Chap1 { // tableaux using System; // classe de test public class P02 { public static void Main() { // un tableau 1 dimension initialis int[] entiers = new int[] { 0, 10, 20, 30 }; for (int i = 0; i < entiers.Length; i++) { Console.Out.WriteLine("entiers[{0}]={1}", i, entiers[i]); }//for // un tableau 2 dimensions initialis double[,] rels = new double[,] { { 0.5, 1.7 }, { 8.4, -6 } }; for (int i = 0; i < rels.GetLength(0); i++) { for (int j = 0; j < rels.GetLength(1); j++) { Console.Out.WriteLine("rels[{0},{1}]={2}", i, j, rels[i, j]); }//for j }//for i // un tableau de tableaux string[][] noms = new string[3][]; for (int i = 0; i < noms.Length; i++) { noms[i] = new string[i + 1]; }//for // initialisation for (int i = 0; i < noms.Length; i++) { for (int j = 0; j < noms[i].Length; j++) { noms[i][j] = "nom" + i + j; }//for j

Les bases du langage C#

14

46. }//for i 47. // affichage 48. for (int i = 0; i < noms.Length; i++) { 49. for (int j = 0; j < noms[i].Length; j++) { 50. Console.Out.WriteLine("noms[{0}][{1}]={2}", i, j, noms[i][j]); 51. }//for j 52. }//for i 53. }//Main 54. }//class 55. }//namespace

A l'excution, nous obtenons les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. entiers[0]=0 entiers[1]=10 entiers[2]=20 entiers[3]=30 rels[0,0]=0,5 rels[0,1]=1,7 rels[1,0]=8,4 rels[1,1]=-6 noms[0][0]=nom00 noms[1][0]=nom10 noms[1][1]=nom11 noms[2][0]=nom20 noms[2][1]=nom21 noms[2][2]=nom22

1.3

Les instructions lmentaires de C#

On distingue 1 2 les instructions lmentaires excutes par l'ordinateur. les instructions de contrle du droulement du programme.

Les instructions lmentaires apparaissent clairement lorsqu'on considre la structure d'un micro-ordinateur et de ses priphriques.
U. C MEMOIRE ECRAN +-------------------+ +-------+ 2 <-+--> 3 +-----------+ 1 ----+------+-> CLAVIER +-----------+--------+--> +-----------+ +-------------------+ +-------+

1. lecture d'informations provenant du clavier 2. traitement d'informations 3. criture d'informations l'cran

1.3.1

Ecriture sur cran

Il existe diffrentes instructions d'criture l'cran :


Console.Out.WriteLine(expression) Console.WriteLine(expression) Console.Error.WriteLine (expression)

o expression est tout type de donne qui puisse tre converti en chane de caractres pour tre affich l'cran. Tous les objets de C# ou .NET ont une mthode ToString() qui est utilise pour faire cette conversion. La classe System.Console donne accs aux oprations d'criture cran (Write, WriteLine). La classe Console a deux proprits Out et Error qui sont des flux d'criture de type TextWriter : Console.WriteLine() est quivalent Console.Out.WriteLine() et crit sur le flux Out associ habituellement l'cran. Console.Error.WriteLine() crit sur le flux Error, habituellement associ lui aussi l'cran.

Les bases du langage C#

15

Les flux Out et Error peuvent tre redirigs vers des fichiers texte au moment de l'excution du programme comme nous le verrons prochainement.

1.3.2

Lecture de donnes tapes au clavier

Le flux de donnes provenant du clavier est dsign par l'objet Console.In de type TextReader. Ce type d'objets permet de lire une ligne de texte avec la mthode ReadLine :
string ligne=Console.In.ReadLine();

La classe Console offre une mthode ReadLine associe par dfaut au flux In. On peut donc crire crire :
string ligne=Console.ReadLine();

La ligne tape au clavier est range dans la variable ligne et peut ensuite tre exploite par le programme. Le flux In peut tre redirig vers un fichier, comme les flux Out et Error.

1.3.3

Exemple d'entres-sorties

Voici un court programme d'illustration des oprations d'entres-sorties clavier/cran :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. using System; namespace Chap1 { // classe de test public class P03 { public static void Main() { // criture sur le flux Out object obj = new object(); Console.Out.WriteLine(obj); // criture sur le flux Error int i = 10; Console.Error.WriteLine("i=" + i); // lecture d'une ligne saisie au clavier Console.Write("Tapez une ligne : "); string ligne = Console.ReadLine(); Console.WriteLine("ligne={0}", ligne); }//fin main }//fin classe }

ligne 9 : obj est une rfrence d'objet ligne 10 : obj est crit l'cran. Par dfaut, c'est la mthode obj.ToString() qui est appele. ligne 14 : on peut aussi crire : Le 1er paramtre "i={0}" est le format d'affichage, les autres paramtres, les expressions afficher. Les lments {n} sont des paramtres "positionnels". A l'excution, le paramtre {n} est remplac par la valeur de l'expression n n.
Console.Error.WriteLine("i={0}",i);

Le rsultat de l'excution est le suivant :


1. 2. 3. 4. System.Object i=10 Tapez une ligne : je suis l ligne=je suis l

ligne 1 : l'affichage produit par la ligne 10 du code. La mthode obj.ToString() a affich le nom du type de la variable obj : System.Object. Le type object est un alias C# du type .NET System.Object.

1.3.4

Redirection des E/S

Il existe sous DOS et UNIX trois priphriques standard appels : 1. priphrique d'entre standard - dsigne par dfaut le clavier et porte le n 0

Les bases du langage C#

16

2. 3.

priphrique de sortie standard - dsigne par dfaut l'cran et porte le n 1 priphrique d'erreur standard - dsigne par dfaut l'cran et porte le n 2

En C#, le flux d'criture Console.Out crit sur le priphrique 1, le flux d'criture Console.Error crit sur le priphrique 2 et le flux de lecture Console.In lit les donnes provenant du priphrique 0. Lorsqu'on lance un programme sous Dos ou Unix, on peut fixer quels seront les priphriques 0, 1 et 2 pour le programme excut. Considrons la ligne de commande suivante :
pg arg1 arg2 .. argn

Derrire les arguments argi du programme pg, on peut rediriger les priphriques d'E/S standard vers des fichiers:
0<in.txt 1>out.txt 1>>out.txt 2>error.txt 2>>error.txt 1>out.txt 2>error.txt

le flux d'entre standard n 0 est redirig vers le fichier in.txt. Dans le programme le flux Console.In prendra donc ses donnes dans le fichier in.txt. redirige la sortie n 1 vers le fichier out.txt. Cela entrane que dans le programme le flux Console.Out crira ses donnes dans le fichier out.txt idem, mais les donnes crites sont ajoutes au contenu actuel du fichier out.txt. redirige la sortie n 2 vers le fichier error.txt. Cela entrane que dans le programme le flux Console.Error crira ses donnes dans le fichier error.txt idem, mais les donnes crites sont ajoutes au contenu actuel du fichier error.txt. Les priphriques 1 et 2 sont tous les deux redirigs vers des fichiers

On notera que pour rediriger les flux d'E/S du programme pg vers des fichiers, le programme pg n'a pas besoin d'tre modifi. C'est le systme d'exploitation qui fixe la nature des priphriques 0,1 et 2. Considrons le programme suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using System; namespace Chap1 { // redirections public class P04 { public static void Main(string[] args) { // lecture flux In string data = Console.In.ReadLine(); // criture flux Out Console.Out.WriteLine("criture dans flux Out : " + data); // criture flux Error Console.Error.WriteLine("criture dans flux Error : " + data); }//Main }//classe }

Gnrons l'excutable de ce code source :


1. 2. 3. C:\data\travail\2007-2008\c# 2008\poly\Chap1\04\bin\Release>dir 29/01/2008 15:01 4 608 04.exe 29/01/2008 15:01 11 776 04.pdb

en [1] : l'excutable est cr par clic droit sur le projet / Build en [2] : dans une fentre Dos, l'excutable 04.exe a t cr dans le dossier bin/Release du projet.

Emettons les commandes suivantes dans la fentre Dos [2] :


1. 2. 3. 4. 5. 6. ...\04\bin\Release>echo test >in.txt ...\04\bin\Release>more in.txt test ...\04\bin\Release>04 0<in.txt 1>out.txt 2>err.txt ...\04\bin\Release>more out.txt criture dans flux Out : test

Les bases du langage C#

17

7. 8.

...\04\bin\Release>more err.txt criture dans flux Error : test

ligne 1 : on met la chane test dans le fichier in.txt lignes 2-3 : on affiche le contenu du fichier in.txt pour vrification ligne 4 : excution du programme 04.exe. Le flux In est redirig vers le fichier in.txt, le flux Out vers le fichier out.txt, le flux Error vers le fichier err.txt. L'excution ne provoque aucun affichage. lignes 5-6 : contenu du fichier out.txt. Ce contenu nous montre que : le fichier in.txt a t lu l'affichage cran a t redirig vers out.txt lignes 7-8 : vrification analogue pour le fichier err.txt

On voit clairement que les flux Out et In n'crivent pas sur les mmes priphriques puisqu'on a pu les rediriger sparment.

1.3.5

Affectation de la valeur d'une expression une variable

On s'intresse ici l'opration variable=expression; L'expression peut tre de type : arithmtique, relationnelle, boolenne, caractres

1.3.5.1

Interprtation de l'opration d'affectation

L'opration variable=expression; est elle-mme une expression dont l'valuation se droule de la faon suivante :

la partie droite de l'affectation est value : le rsultat est une valeur V. la valeur V est affecte la variable la valeur V est aussi la valeur de l'affectation vue cette fois en tant qu'expression.

C'est ainsi que l'opration V1=V2=expression est lgale. A cause de la priorit, c'est l'oprateur = le plus droite qui va tre valu. On a donc V1=(V2=expression) L'expression V2=expression est value et a pour valeur V. L'valuation de cette expression a provoqu l'affectation de V V2. L'oprateur = suivant est alors valu sous la forme : V1=V La valeur de cette expression est encore V. Son valuation provoque l'affectation de V V1. Ainsi donc, l'opration V1=V2=expression est une expression dont l'valuation 1 2 provoque l'affectation de la valeur de expression aux variables V1 et V2 rend comme rsultat la valeur de expression.

On peut gnraliser une expression du type : V1=V2=....=Vn=expression

1.3.5.2

Expression arithmtique

Les oprateurs des expressions arithmtiques sont les suivants : + addition

Les bases du langage C#

18

* / %

soustraction multiplication division : le rsultat est le quotient exact si l'un au moins des oprandes est rel. Si les deux oprandes sont entiers le rsultat est le quotient entier. Ainsi 5/2 -> 2 et 5.0/2 ->2.5. division : le rsultat est le reste quelque soit la nature des oprandes, le quotient tant lui entier. C'est donc l'opration modulo.

Il existe diverses fonctions mathmatiques. En voici quelques-unes :


double double double double double double double double Sqrt(double x) Cos(double x) Sin(double x) Tan(double x) Pow(double x,double y) Exp(double x) Log(double x) Abs(double x)

racine carre Cosinus Sinus Tangente x la puissance y (x>0) Exponentielle Logarithme nprien valeur absolue

etc... Toutes ces fonctions sont dfinies dans une classe C# appele Math. Lorsqu'on les utilise, il faut les prfixer avec le nom de la classe o elles sont dfinies. Ainsi on crira :
double x, y=4; x=Math.Sqrt(y);

La dfinition complte de la classe Math est la suivante :

Les bases du langage C#

19

1.3.5.3

Priorits dans l'valuation des expressions arithmtiques

La priorit des oprateurs lors de l'valuation d'une expression arithmtique est la suivante (du plus prioritaire au moins prioritaire) : [fonctions], [ ( )],[ *, /, %], [+, -] Les oprateurs d'un mme bloc [ ] ont mme priorit.

1.3.5.4

Expressions relationnelles

Les oprateurs sont les suivants : <, <=, ==, !=, >, >= priorits des oprateurs 1. >, >=, <, <= 2. ==, != Le rsultat d'une expression relationnelle est le boolen false si expression est fausse, true sinon.
bool fin; int x=...; fin=x>4;

Comparaison de deux caractres Soient deux caractres C1 et C2. Il est possible de les comparer avec les oprateurs <, <=, ==, !=, >, >= Ce sont alors leurs codes Unicode, qui sont des nombres, qui sont alors compars. Selon l'ordre Unicode on a les relations suivantes : espace < .. < '0' < '1' < .. < '9' < .. < 'A' < 'B' < .. < 'Z' < .. < 'a' < 'b' < .. <'z' Comparaison de deux chanes de caractres Elles sont compares caractre par caractre. La premire ingalit rencontre entre deux caractres induit une ingalit de mme sens sur les chanes. Exemples : Soit comparer les chanes "Chat" et "Chien"

Les bases du langage C#

20

"Chat" "Chien" ----------------------'C' = 'C' 'h' = 'h' 'a' < 'i' Cette dernire ingalit permet de dire que "Chat" < "Chien". Soit comparer les chanes "Chat" et "Chaton". Il y a galit tout le temps jusqu' puisement de la chane "Chat". Dans ce cas, la chane puise est dclare la plus "petite". On a donc la relation "Chat" < "Chaton". Fonctions de comparaison de deux chanes On peut utiliser les oprateurs relationnels == et != pour tester l'galit ou non de deux chanes, ou bien la mthode Equals de la classe System.String. Pour les relations < <= > >=, il faut utiliser la mthode CompareTo de la classe System.String :
1. using System; 2. 3. namespace Chap1 { 4. class P05 { 5. static void Main(string[] args) { 6. string chaine1="chat", chaine2="chien"; 7. int n = chaine1.CompareTo(chaine2); 8. bool egal = chaine1.Equals(chaine2); 9. Console.WriteLine("i={0}, egal={1}", n, egal); 10. Console.WriteLine("chien==chaine1:{0},chien!=chaine2:{1}", "chien"==chaine1,"chien" != chaine2); 11. } 12. } 13. }

Ligne 7, la variable i aura la valeur : 0 si les deux chanes sont gales 1 si chane n1 > chane n2 -1 si chane n1 < chane n2 Ligne 8, la variable egal aura la valeur true si les deux chanes sont gales, false sinon. Ligne 10, on utilise les oprateurs == et != pour vrifier l'galit ou non de deux chanes. Les rsultats de l'excution :
1. 2. i=-1, egal=False chien==chaine1:False,chien!=chaine2:False

1.3.5.5

Expressions boolennes

Les oprateurs utilisables sont AND (&&) OR(||) NOT (!). Le rsultat d'une expression boolenne est un boolen. priorits des oprateurs : 1. ! 2. && 3. ||
double x = 3.5; bool valide = x > 2 && x < 4;

Les oprateurs relationnels ont priorit sur les oprateurs && et ||.

1.3.5.6

Traitement de bits

Les oprateurs

Les bases du langage C#

21

Soient i et j deux entiers.


i<<n i>>n i & j i | j ~i i^j

dcale i de n bits sur la gauche. Les bits entrants sont des zros. dcale i de n bits sur la droite. Si i est un entier sign (signed char, int, long) le bit de signe est prserv. fait le ET logique de i et j bit bit. fait le OU logique de i et j bit bit. complmente i 1 fait le OU EXCLUSIF de i et j

Soit le code suivant :


1. 2. 3. 4. short i = 100, j = -13; ushort k = 0xF123; Console.WriteLine("i=0x{0:x4}, j=0x{1:x4}, k=0x{2:x4}", i,j,k); Console.WriteLine("i<<4=0x{0:x4}, i>>4=0x{1:x4},k>>4=0x{2:x4},i&j=0x{3:x4},i| j=0x{4:x4},~i=0x{5:x4},j<<2=0x{6:x4},j>>2=0x{7:x4}", i << 4, i >> 4, k >> 4, (short)(i & j), (short)(i | j), (short)(~i), (short)(j << 2), (short)(j >> 2));

le format {0:x4} affiche le paramtre n 0 au format hexadcimal (x) avec 4 caractres (4).

Les rsultats de l'excution sont les suivants :


1. 2. i=0x0064, j=0xfff3, k=0xf123 i<<4=0x0640, i>>4=0x0006,k>>4=0x0f12,i&j=0x0060,i|j=0xfff7,~i=0xff9b,j<<2=0xffcc,j>>2=0xfffc

1.3.5.7

Combinaison d'oprateurs

a=a+b peut s'crire a+=b a=a-b peut s'crire a-=b Il en est de mme avec les oprateurs /, %,* ,<<, >>, &, |, ^. Ainsi a=a/2; peut s'crire a/=2;

1.3.5.8

Oprateurs d'incrmentation et de dcrmentation

La notation variable++ signifie variable=variable+1 ou encore variable+=1 La notation variable-- signifie variable=variable-1 ou encore variable-=1.

1.3.5.9

L'oprateur ternaire ?

L'expression
expr_cond ? expr1:expr2

est value de la faon suivante : 1 2 3 l'expression expr_cond est value. C'est une expression conditionnelle valeur vrai ou faux Si elle est vraie, la valeur de l'expression est celle de expr1 et expr2 n'est pas value. Si elle est fausse, c'est l'inverse qui se produit : la valeur de l'expression est celle de expr2 et expr1 n'est pas value.

L'opration i=(j>4 ? j+1:j-1); affectera la variable i : j+1 si j>4, j-1 sinon. C'est la mme chose que d'crire if(j>4) i=j+1; else i=j-1; mais c'est plus concis.

1.3.5.10

Priorit gnrale des oprateurs


gd dg dg gd gd gd gd gd gd gd

() [] fonction ! ~ ++ -new (type) oprateurs cast * / % + << >> < <= > >= instanceof == != & ^

Les bases du langage C#

22

| && || ? : = += -= etc. .

gd gd gd dg dg

gd indique qu'a priorit gale, c'est la priorit gauche-droite qui est observe. Cela signifie que lorsque dans une expression, l'on a des oprateurs de mme priorit, c'est l'oprateur le plus gauche dans l'expression qui est valu en premier. dg indique une priorit droite-gauche.

1.3.5.11

Les changements de type

Il est possible, dans une expression, de changer momentanment le codage d'une valeur. On appelle cela changer le type d'une donne ou en anglais type casting. La syntaxe du changement du type d'une valeur dans une expression est la suivante: (type) valeur La valeur prend alors le type indiqu. Cela entrane un changement de codage de la valeur.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. using System; namespace Chap1 { class P06 { static void Main(string[] args) { int i = 3, j = 4; float f1=i/j; float f2=(float)i/j; Console.WriteLine("f1={0}, f2={1}",f1,f2); } } }

ligne 7, f1 aura la valeur 0.0. La division 3/4 est une division entire puisque les deux oprandes sont de type int. ligne 8, (float)i est la valeur de i transforme en float. Maintenant, on a une division entre un rel de type float et un entier de type int. C'est la division entre nombres rels qui est alors faite. La valeur de j sera elle galement transforme en type float, puis la division des deux rels sera faite. f2 aura alors la valeur 0,75.

Voici les rsultats de l'excution :


f1=0, f2=0,75

Dans l'opration (float)i : i est une valeur code de faon exacte sur 2 octets (float) i est la mme valeur code de faon approche en rel sur 4 octets Il y a donc transcodage de la valeur de i. Ce transcodage n'a lieu que le temps d'un calcul, la variable i conservant toujours son type int.

1.4
1.4.1

Les instructions de contrle du droulement du programme


Arrt

La mthode Exit dfinie dans la classe Environment permet d'arrter l'excution d'un programme.
syntaxe void Exit(int status) action arrte le processus en cours et rend la valeur status au processus pre

Exit provoque la fin du processus en cours et rend la main au processus appelant. La valeur de status peut tre utilise par celui-ci. Sous DOS, cette variable status est rendue dans la variable systme ERRORLEVEL dont la valeur peut tre teste dans un fichier batch. Sous Unix, avec l'interprteur de commandes Shell Bourne, c'est la variable $? qui rcupre la valeur de status.
Environment.Exit(0);

Les bases du langage C#

23

arrtera l'excution du programme avec une valeur d'tat 0.

1.4.2

Structure de choix simple


if (condition) {actions_condition_vraie;} else {actions_condition_fausse;}

syntaxe :

notes:

la condition est entoure de parenthses. chaque action est termine par point-virgule. les accolades ne sont pas termines par point-virgule. les accolades ne sont ncessaires que s'il y a plus d'une action. la clause else peut tre absente. il n'y a pas de clause then.

L'quivalent algorithmique de cette structure est la structure si .. alors sinon : si condition alors actions_condition_vraie sinon actions_condition_fausse finsi exemple
if (x>0) { nx=nx+1;sx=sx+x;} else dx=dx-x;

On peut imbriquer les structures de choix :


if(condition1) if (condition2) {......} else //condition2 {......} else //condition1 {.......}

Se pose parfois le problme suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. using System; namespace Chap1 { class P07 { static void Main(string[] args) { int n = 5; if (n > 1) if (n > 6) Console.Out.WriteLine(">6"); else Console.Out.WriteLine("<=6"); } } }

Dans l'exemple prcdent, le else de la ligne 10 se rapporte quel if ? La rgle est qu'un else se rapporte toujours au if le plus proche : if(n>6), ligne 8, dans l'exemple. Considrons un autre exemple :
1. 2. 3. if (n2 > 1) { if (n2 > 6) Console.Out.WriteLine(">6"); } else Console.Out.WriteLine("<=1");

Ici nous voulions mettre un else au if(n2>1) et pas de else au if(n2>6). A cause de la remarque prcdente, nous sommes obligs de mettre des accolades au if(n2>1) {...} else ...

1.4.3

Structure de cas

La syntaxe est la suivante :

Les bases du langage C#

24

switch(expression) { case v1: actions1; break; case v2: actions2; break; . .. .. .. .. .. default: actions_sinon; break; }

notes

la valeur de l'expression de contrle du switch peut tre un entier, un caractre, une chane de caractres l'expression de contrle est entoure de parenthses. la clause default peut tre absente. les valeurs vi sont des valeurs possibles de l'expression. Si l'expression a pour valeur vi , les actions derrire la clause case vi sont excutes. l'instruction break fait sortir de la structure de cas. chaque bloc d'instructions li une valeur vi doit se terminer par une instruction de branchement (break, goto, return, ...) sinon le compilateur signale une erreur.

exemple En algorithmique
selon la valeur de choix cas 0 fin du module cas 1 excuter module M1 cas 2 excuter module M2 sinon erreur<--vrai findescas

En C#
1. int choix = 2; 2. bool erreur = false; 3. switch (choix) { 4. case 0: return; 5. case 1: M1(); break; 6. case 2: M2(); break; 7. default: erreur = true; break; 8. } 9. }// fin Main 10. 11. static void M1() { 12. Console.WriteLine("M1"); 13. } 14. 15. static void M2() { 16. Console.WriteLine("M2"); 17. } 18. }

1.4.4
1.4.4.1

Structures de rptition
Nombre de rptitions connu

Structure for La syntaxe est la suivante :


for (i=id;i<=if;i=i+ip){

Les bases du langage C#

25

actions; }

Notes

les 3 arguments du for sont l'intrieur d'une parenthse et spars par des points-virgules. chaque action du for est termine par un point-virgule. l'accolade n'est ncessaire que s'il y a plus d'une action. l'accolade n'est pas suivie de point-virgule.

L'quivalent algorithmique est la structure pour : pour i variant de id if avec un pas de ip actions finpour qu'on peut traduire par une structure tantque : i id tantque i<=if actions i i+ip fintantque Structure foreach La syntaxe est la suivante :
foreach (Type variable in collection) instructions; }

Notes

collection est une collection d'objets numrable. La collection d'objets numrable que nous connaissons dj est le tableau Type est le type des objets de la collection. Pour un tableau, ce serait le type des lments du tableau variable est une variable locale la boucle qui va prendre successivement pour valeur, toutes les valeurs de la collection.

Ainsi le code suivant : 1.


2. 3. 4. string[] amis = { "paul", "hlne", "jacques", "sylvie" }; foreach (string nom in amis) { Console.WriteLine(nom); }

afficherait :
paul hlne jacques sylvie

1.4.4.2

Nombre de rptitions inconnu

Il existe de nombreuses structures en C# pour ce cas. Structure tantque (while)


while(condition){ actions; }

On boucle tant que la condition est vrifie. La boucle peut ne jamais tre excute.

Les bases du langage C#

26

notes:

la condition est entoure de parenthses. chaque action est termine par point-virgule. l'accolade n'est ncessaire que s'il y a plus d'une action. l'accolade n'est pas suivie de point-virgule.

La structure algorithmique correspondante est la structure tantque : tantque condition actions fintantque Structure rpter jusqu' (do while) La syntaxe est la suivante :
do{ instructions; }while(condition);

On boucle jusqu' ce que la condition devienne fausse. Ici la boucle est faite au moins une fois. notes

la condition est entoure de parenthses. chaque action est termine par point-virgule. l'accolade n'est ncessaire que s'il y a plus d'une action. l'accolade n'est pas suivie de point-virgule.

La structure algorithmique correspondante est la structure rpter jusqu' : rpter actions jusqu' condition Structure pour gnrale (for) La syntaxe est la suivante :
for(instructions_dpart;condition;instructions_fin_boucle){ instructions; }

On boucle tant que la condition est vraie (value avant chaque tour de boucle). Instructions_dpart sont effectues avant d'entrer dans la boucle pour la premire fois. Instructions_fin_boucle sont excutes aprs chaque tour de boucle. notes

les diffrentes instructions dans instructions_depart et instructions_fin_boucle sont spares par des virgules.

La structure algorithmique correspondante est la suivante : instructions_dpart tantque condition actions instructions_fin_boucle fintantque Exemples Les fragments de code suivants calculent tous la somme des 10 premiers nombres entiers.

Les bases du langage C#

27

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13.

int i, somme, n=10; for (i = 1, somme = 0; i <= n; i = i + 1) somme = somme + i; for (i = 1, somme = 0; i <= n; somme = somme + i, i = i + 1) ; i = 1; somme = 0; while (i <= n) { somme += i; i++; } i = 1; somme = 0; do somme += i++; while (i <= n);

1.4.4.3
break continue

Instructions de gestion de boucle


fait sortir de la boucle for, while, do ... while. fait passer l'itration suivante des boucles for, while, do ... while

1.5

La gestion des exceptions

De nombreuses fonctions C# sont susceptibles de gnrer des exceptions, c'est dire des erreurs. Lorsqu'une fonction est susceptible de gnrer une exception, le programmeur devrait la grer dans le but d'obtenir des programmes plus rsistants aux erreurs : il faut toujours viter le "plantage" sauvage d'une application. La gestion d'une exception se fait selon le schma suivant : try{ code susceptible de gnrer une exception } catch (Exception e){ traiter l'exception e } instruction suivante Si la fonction ne gnre pas d'exception, on passe alors instruction suivante, sinon on passe dans le corps de la clause catch puis instruction suivante. Notons les points suivants :

e est un objet de type Exception ou driv. On peut tre plus prcis en utilisant des types tels que IndexOutOfRangeException, FormatException, SystemException, etc : il existe plusieurs types d'exceptions. En crivant catch (Exception e), on indique qu'on veut grer toutes les types d'exceptions. Si le code de la clause try est susceptible de gnrer plusieurs types d'exceptions, on peut vouloir tre plus prcis en grant l'exception avec plusieurs clauses catch : try{ code susceptible de gnrer les exceptions } catch ( IndexOutOfRangeException e1){ traiter l'exception e1 } } catch ( FormatException e2){ traiter l'exception e2 } instruction suivante

On peut ajouter aux clauses try/catch, une clause finally : try{ code susceptible de gnrer une exception } catch (Exception e){ traiter l'exception e } finally{ code excut aprs try ou catch

Les bases du langage C#

28

} instruction suivante Qu'il y ait exception ou pas, le code de la clause finally sera toujours excut. Dans la clause catch, on peut ne pas vouloir utiliser l'objet Exception disponible. Au lieu d'crire catch (Exception e){..}, on crit alors catch(Exception){...} ou plus simplement catch {...}. La classe Exception a une proprit Message qui est un message dtaillant l'erreur qui s'est produite. Ainsi si on veut afficher celui-ci, on crira : catch (Exception ex){ Console.WriteLine("L'erreur suivante s'est produite : {0}",ex.Message); ... }//catch La classe Exception a une mthode ToString qui rend une chane de caractres indiquant le type de l'exception ainsi que la valeur de la proprit Message. On pourra ainsi crire : catch (Exception ex){ Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.ToString()); ... }//catch On peut crire aussi : catch (Exception ex){ Console.WriteLine("L'erreur suivante s'est produite : {0}",ex); ... }//catch Le compilateur va attribuer au paramtre {0}, la valeur ex.ToString(). L'exemple suivant montre une exception gnre par l'utilisation d'un lment de tableau inexistant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; namespace Chap1 { class P08 { static void Main(string[] args) { // dclaration & initialisation d'un tableau int[] tab = { 0, 1, 2, 3 }; int i; // affichage tableau avec un for for (i = 0; i < tab.Length; i++) Console.WriteLine("tab[{0}]={1}", i, tab[i]); // affichage tableau avec un for each foreach (int lmt in tab) { Console.WriteLine(lmt); } // gnration d'une exception try { tab[100] = 6; } catch (Exception e) { Console.Error.WriteLine("L'erreur suivante s'est produite : " + e); return; }//try-catch finally { Console.WriteLine("finally ..."); } } } }

Ci-dessus, la ligne 18 va gnrer une exception parce que le tableau tab n'a pas d'lment n 100. L'excution du programme donne les rsultats suivants :
1. 2. 3. 4. 5. 6. 7. 8. tab[0]=0 tab[1]=1 tab[2]=2 tab[3]=3 0 1 2 3

Les bases du langage C#

29

9.

L'erreur suivante s'est produite : System.IndexOutOfRangeException: L'index se trouve en dehors des limites du tableau. 10. Chap1.P08.Main(String[] args) dans C:\data\travail\2007-2008\c# 2008\poly\Chap1\08\Program.cs:ligne 7 11. finally ...

ligne 9 : l'exception [System.IndexOutOfRangeException] s'est produite ligne 11 : la clause finally (lignes 23-25) du code a t excute, alors mme que ligne 21, on avait une instruction return pour sortir de la mthode. On retiendra que la clause finally est toujours excute.

Voici un autre exemple o on gre l'exception provoque par l'affectation d'une chane de caractres un variable de type entier lorsque la chane ne reprsente pas un nombre entier :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. using System; namespace Chap1 { class P08 { static void Main(string[] args) { // exemple 2 // On demande le nom Console.Write("Nom : "); // lecture rponse string nom = Console.ReadLine(); // on demande l'ge int age = 0; bool ageOK = false; while (!ageOK) { // question Console.Write("ge : "); // lecture-vrification rponse try { age = int.Parse(Console.ReadLine()); ageOK = age>=1; } catch { }//try-catch if (!ageOK) { Console.WriteLine("Age incorrect, recommencez..."); } }//while // affichage final Console.WriteLine("Vous vous appelez {0} et vous avez {1} an(s)",nom,age);

} } }

lignes 15-27 : la boucle de saisie de l'ge d'une personne ligne 20 : la ligne tape au clavier est transforme en nombre entier par la mthode int.Parse. Cette mthode lance une exception si la conversion n'est pas possible. C'est pourquoi, l'opration a t place dans un try / catch. lignes 22-23 : si une exception est lance, on va dans le catch o rien n'est fait. Ainsi, le boolen ageOK positionn false, ligne 14, va-t-il rester false. ligne 21 : si on arrive cette ligne, c'est que la conversion string -> int a russi. On vrifie cependant que l'entier obtenu est bien suprieur ou gal 1. lignes 24-26 : un message d'erreur est mis si l'ge est incorrect.

Quelques rsultats d'excution :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. Nom : dupont ge : 23 Vous vous appelez dupont et vous avez 23 an(s) Nom : durand ge : x Age incorrect, recommencez... ge : -4 Age incorrect, recommencez... ge : 12 Vous vous appelez durand et vous avez 12 an(s)

Les bases du langage C#

30

1.6

Application exemple - V1

On se propose d'crire un programme permettant de calculer l'impt d'un contribuable. On se place dans le cas simplifi d'un contribuable n'ayant que son seul salaire dclarer (chiffres 2004 pour revenus 2003) :

on calcule le nombre de parts du salari nbParts=nbEnfants/2 +1 s'il n'est pas mari, nbEnfants/2+2 s'il est mari, o nbEnfants est son nombre d'enfants. s'il a au moins trois enfants, il a une demi part de plus on calcule son revenu imposable R=0.72*S o S est son salaire annuel on calcule son coefficient familial QF=R/nbParts on calcule son impt I. Considrons le tableau suivant :
4262 8382 14753 23888 38868 47932 0 0 0.0683 0.1914 0.2826 0.3738 0.4262 0.4809 0 291.09 1322.92 2668.39 4846.98 6883.66 9505.54

Chaque ligne a 3 champs. Pour calculer l'impt I, on recherche la premire ligne o QF<=champ1. Par exemple, si QF=5000 on trouvera la ligne 8382 0.0683 291.09 L'impt I est alors gal 0.0683*R - 291.09*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vrifie, alors ce sont les coefficients de la dernire ligne qui sont utiliss. Ici : 0 0.4809 9505.54 ce qui donne l'impt I=0.4809*R - 9505.54*nbParts. Le programme C# correspondant est le suivant :
1. using System; 2. 3. namespace Chap1 { 4. class Impots { 5. static void Main(string[] args) { 6. // tableaux de donnes ncessaires au calcul de l'impt 7. decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M }; 8. decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M }; 9. decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M }; 10. 11. // on rcupre le statut marital 12. bool OK = false; 13. string reponse = null; 14. while (!OK) { 15. Console.Write("Etes-vous mari(e) (O/N) ? "); 16. reponse = Console.ReadLine().Trim().ToLower(); 17. if (reponse != "o" && reponse != "n") 18. Console.Error.WriteLine("Rponse incorrecte. Recommencez"); 19. else OK = true; 20. }//while 21. bool marie = reponse == "o"; 22. 23. // nombre d'enfants 24. OK = false; 25. int nbEnfants = 0; 26. while (!OK) { 27. Console.Write("Nombre d'enfants : "); 28. try { 29. nbEnfants = int.Parse(Console.ReadLine()); 30. OK = nbEnfants >= 0; 31. } catch { 32. }// try 33. if (!OK) { 34. Console.WriteLine("Rponse incorrecte. Recommencez"); 35. } 36. }// while 37. 38. // salaire 39. OK = false; 40. int salaire = 0; 41. while (!OK) {

Les bases du langage C#

31

42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. } 78. }

Console.Write("Salaire annuel : "); try { salaire = int.Parse(Console.ReadLine()); OK = salaire >= 0; } catch { }// try if (!OK) { Console.WriteLine("Rponse incorrecte. Recommencez"); } }// while // calcul du nombre de parts decimal nbParts; if (marie) nbParts = (decimal)nbEnfants / 2 + 2; else nbParts = (decimal)nbEnfants / 2 + 1; if (nbEnfants >= 3) nbParts += 0.5M; // revenu imposable decimal revenu = 0.72M * salaire; // quotient familial decimal QF = revenu / nbParts; // recherche de la tranche d'impots correspondant QF int i; int nbTranches = limites.Length; limites[nbTranches - 1] = QF; i = 0; while (QF > limites[i]) i++; // l'impt int impots = (int)(coeffR[i] * revenu - coeffN[i] * nbParts); // on affiche le rsultat Console.WriteLine("Impt payer : {0} euros", impots);

lignes 7-9 : les valeurs numriques sont suffixes par M (Money) pour qu'elles soient de type decimal. ligne 16 : Console.ReadLine() rend la chane C1 tape au clavier C1.Trim() enlve les espaces de dbut et fin de C1 - rend une chane C2 C2.ToLower() rend la chane C3 qui est la chane C2 transforme en minuscules. ligne 21 : le boolen marie reoit la valeur true ou false de la relation reponse=="o" ligne 29 : la chane tape au clavier est transforme en type int. Si la transformation choue, une exception est lance. ligne 30 : le boolen OK reoit la valeur true ou false de la relation nbEnfants>=0 lignes 55-56 : on ne peut crire simplement nbEnfants/2. Si nbEnfants tait gal 3, on aurait 3/2, une division entire qui donnerait 1 et non 1.5. Aussi, crit-on (decimal)nbEnfants pour rendre rel l'un des oprandes de la division et avoir ainsi une division entre rels.

Voici des exemples d'excution :


Etes-vous mari(e) (O/N) ? o Nombre d'enfants : 2 Salaire annuel : 60000 Impt payer : 4282 euros Etes-vous mari(e) (O/N) ? oui Rponse incorrecte. Recommencez Etes-vous mari(e) (O/N) ? o Nombre d'enfants : trois Rponse incorrecte. Recommencez Nombre d'enfants : 3 Salaire annuel : 60000 euros Rponse incorrecte. Recommencez Salaire annuel : 60000 Impt payer : 2959 euros

1.7

Arguments du programme principal

Les bases du langage C#

32

La fonction principale Main peut admettre comme paramtre un tableau de chanes : String[] (ou string[]). Ce tableau contient les arguments de la ligne de commande utilise pour lancer l'application. Ainsi si on lance le programme P avec la commande (Dos) suivante : P arg0 arg1 argn et si la fonction Main est dclare comme suit :
public static void Main(string[] args)

on aura args[0]="arg0", args[1]="arg1" Voici un exemple :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. using System; namespace Chap1 { class P10 { static void Main(string[] args) { // on liste les paramtres reus Console.WriteLine("Il y a " + args.Length + " arguments"); for (int i = 0; i < args.Length; i++) { Console.Out.WriteLine("arguments[" + i + "]=" + args[i]); } } } }

Pour passer des arguments au code excut, on procdera comme suit :

1 3 2

en [1] : clic droit sur le projet / Properties en [2] : onglet [Debug] en [3] : mettre les arguments

L'excution donne les rsultats suivants :


1. 2. 3. 4. 5. Il y a 4 arguments arguments[0]=a0 arguments[1]=a1 arguments[2]=a2 arguments[3]=a3

On notera que la signature


public static void Main()

est valide si la fonction Main n'attend pas de paramtres.

1.8

Les numrations

Une numration est un type de donnes dont le domaine de valeurs est un ensemble de constantes entires. Considrons un programme qui a grer des mentions un examen. Il y en aurait cinq : Passable,AssezBien,Bien,TrsBien, Excellent. On pourrait alors dfinir une numration pour ces cinq constantes :

Les bases du langage C#

33

enum Mentions { Passable, AssezBien, Bien, TrsBien, Excellent };

De faon interne, ces cinq constantes sont codes par des entiers conscutifs commenant par 0 pour la premire constante, 1 pour la suivante, etc... Une variable peut tre dclare comme prenant ces valeurs dans l'numration :
1. 2. // une variable qui prend ses valeurs dans l'numration Mentions Mentions maMention = Mentions.Passable;

On peut comparer une variable aux diffrentes valeurs possibles de l'numration :


1. 2. 3. if (maMention == Mentions.Passable) { Console.WriteLine("Peut mieux faire"); }

On peut obtenir toutes les valeurs de l'numration :


1. 2. 3. 4. // liste des mentions sous forme de chanes foreach (Mentions m in Enum.GetValues(maMention.GetType())) { Console.WriteLine(m);

De la mme faon que le type simple int est quivalent la structure System.Int32, le type simple enum est quivalent la structure System.Enum. Cette structure a une mthode statique GetValues qui permet d'obtenir toutes les valeurs d'un type numr que l'on passe en paramtre. Celui-ci doit tre un objet de type Type qui est une classe d'informations sur le type d'une donne. Le type d'une variable v est obtenu par v.GetType(). Le type d'un type T est obtenu par typeof(T). Donc ici maMention.GetType() donne l'objet Type de l'numration Mentions et Enum.GetValues(maMention.GetType()) la liste des valeurs de l'numration Mentions. Si on crit maintenant
1. 2. 3. 4. //liste des mentions sous forme d'entiers foreach (int m in Enum.GetValues(typeof(Mentions))) { Console.WriteLine(m); }

Ligne 2, la variable de boucle est de type entier. On obtient alors la liste des valeurs de l'numration sous forme d'entiers. L'objet de type System.Type correspondant au type de donnes Mentions est obtenu par typeof(Mentions). On aurait pu crire comme prcdemment, maMention.GetType(). Le programme suivant met en lumire ce qui vient d'tre crit :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. using System; namespace Chap1 { class P11 { enum Mentions { Passable, AssezBien, Bien, TrsBien, Excellent }; static void Main(string[] args) { // une variable qui prend ses valeurs dans l'numration Mentions Mentions maMention = Mentions.Passable; // affichage valeur variable Console.WriteLine("mention=" + maMention); // test avec valeur de l'numration if (maMention == Mentions.Passable) { Console.WriteLine("Peut mieux faire"); } // liste des mentions sous forme de chanes foreach (Mentions m in Enum.GetValues(maMention.GetType())) { Console.WriteLine(m); } //liste des mentions sous forme d'entiers foreach (int m in Enum.GetValues(typeof(Mentions))) { Console.WriteLine(m); } } } }

Les rsultats d'excution sont les suivants :


1. mention=Passable

Les bases du langage C#

34

2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12.

Peut mieux faire Passable AssezBien Bien TrsBien Excellent 0 1 2 3 4

1.9

Passage de paramtres une fonction

Nous nous intressons ici au mode de passage des paramtres d'une fonction. Considrons la fonction statique suivante :
1. 2. 3. 4. private static void ChangeInt(int a) { a = 30; Console.WriteLine("Paramtre formel a=" + a); }

Dans la dfinition de la fonction, ligne1, a est appel un paramtre formel. Il n'est l que pour les besoins de la dfinition de la fonction changeInt. Il aurait tout aussi bien pu s'appeler b. Considrons maintenant une utilisation de cette fonction :
1. 2. 3. 4. 5. public static void Main() { int age = 20; ChangeInt(age); Console.WriteLine("Paramtre effectif age=" + age);

Ici dans l'instruction de la ligne 3, ChangeInt(age), age est le paramtre effectif qui va transmettre sa valeur au paramtre formel a. Nous nous intressons la faon dont un paramtre formel rcupre la valeur d'un paramtre effectif.

1.9.1

Passage par valeur

L'exemple suivant nous montre que les paramtres d'une fonction sont par dfaut passs par valeur, c'est dire que la valeur du paramtre effectif est recopie dans le paramtre formel correspondant. On a deux entits distinctes. Si la fonction modifie le paramtre formel, le paramtre effectif n'est lui en rien modifi.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. using System; namespace Chap1 { class P12 { public static void Main() { int age = 20; ChangeInt(age); Console.WriteLine("Paramtre effectif age=" + age); } private static void ChangeInt(int a) { a = 30; Console.WriteLine("Paramtre formel a=" + a); } } }

Les rsultats obtenus sont les suivants :


1. 2. Paramtre formel a=30 Paramtre effectif age=20

La valeur 20 du paramtre effectif age a t recopie dans le paramtre formel a (ligne 10). Celui-ci a t ensuite modifi (ligne 11). Le paramtre effectif est lui rest inchang. Ce mode de passage convient aux paramtres d'entre d'une fonction.

1.9.2

Passage par rfrence

Les bases du langage C#

35

Dans un passage par rfrence, le paramtre effectif et le paramtre formel sont une seule et mme entit. Si la fonction modifie le paramtre formel, le paramtre effectif est lui aussi modifi. En C#, ils doivent tre tous deux prcds du mot cl ref : Voici un exemple :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using System; namespace Chap1 { class P12 { public static void Main() { // exemple 2 int age2 = 20; ChangeInt2(ref age2); Console.WriteLine("Paramtre effectif age2=" + age2); } private static void ChangeInt2(ref int a2) { a2 = 30; Console.WriteLine("Paramtre formel a2=" + a2); } } }

et les rsultats d'excution :


1. 2. Paramtre formel a2=30 Paramtre effectif age2=30

Le paramtre effectif a suivi la modification du paramtre formel. Ce mode de passage convient aux paramtres de sortie d'une fonction.

1.9.3

Passage par rfrence avec le mot cl out

Considrons l'exemple prcdent dans lequel la variable age2 ne serait pas initialise avant l'appel la fonction changeInt :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using System; namespace Chap1 { class P12 { public static void Main() { // exemple 2 int age2; ChangeInt2(ref age2); Console.WriteLine("Paramtre effectif age2=" + age2); } private static void ChangeInt2(ref int a2) { a2 = 30; Console.WriteLine("Paramtre formel a2=" + a2); } } }

Lorsqu'on compile ce programme, on a une erreur :


Use of unassigned local variable 'age2'

On peut contourner l'obstacle en affectant une valeur initiale age2. On peut aussi remplacer le mot cl ref par le mot cl out. On exprime alors que la paramtre est uniquement un paramtre de sortie et n'a donc pas besoin de valeur initiale :
1. using System; 2. 3. namespace Chap1 { 4. class P12 { 5. public static void Main() { 6. // exemple 3 7. int age3; 8. ChangeInt3(out age3); 9. Console.WriteLine("Paramtre effectif age3=" + age3); 10. } 11. private static void ChangeInt3(out int a3) {

Les bases du langage C#

36

12. 13. 14. 15. } 16. }

a3 = 30; Console.WriteLine("Paramtre formel a3=" + a3);

Les rsultats de l'excution sont les suivants :


1. 2. Paramtre formel a3=30 Paramtre effectif age3=30

Les bases du langage C#

37

2
2.1

Classes, Stuctures, Interfaces


L' objet par l'exemple
Gnralits

2.1.1

Nous abordons maintenant, par l'exemple, la programmation objet. Un objet est une entit qui contient des donnes qui dfinissent son tat (on les appelle des champs, attributs, ...) et des fonctions (on les appelle des mthodes). Un objet est cr selon un modle qu'on appelle une classe :
public class C1{ Type1 p1; Type2 p2; Type3 m3(){ } Type4 m4(){ } } // champ p1 // champ p2 // mthode m3 // mthode m4

A partir de la classe C1 prcdente, on peut crer de nombreux objets O1, O2, Tous auront les champs p1, p2, et les mthodes m3, m4, Mais ils auront des valeurs diffrentes pour leurs champs pi ayant ainsi chacun un tat qui leur est propre. Si o1 est un objet de type C1, o1.p1 dsigne la proprit p1 de o1 et o1.m1 la mthode m1 de O1. Considrons un premier modle d'objet : la classe Personne.

2.1.2

Cration du projet C#

Dans les exemples prcdents, nous n'avions dans un projet qu'un unique fichier source : Program.cs. A partir de maintenant, nous pourrons avoir plusieurs fichiers source dans un mme projet. Nous montrons comment procder.

2 1

En [1], crez un nouveau projet. En [2], choisissez une Application Console. En [3], laissez la valeur par dfaut. En [4], validez. En [5], le projet qui a t gnr. Le contenu de Program.cs est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. using using using using System; System.Collections.Generic; System.Linq; System.Text;

namespace ConsoleApplication1 { class Program { static void Main(string[] args) {

Classes, Stuctures, Interfaces

38

9. 10. } 11. }

Sauvegardons le projet cr : 3 2 4 5 6

En [1], l'option de sauvegarde. En [2], dsignez le dossier o sauvegarder le projet. En [3], donnez un nom au projet. En [5], indiquez que vous voulez crer une solution. Une solution est un ensemble de projets. En [4], donnez le nom de la solution. En [6], validez la sauvegarde.

1 2

En [1], le projet sauvegard. En [2], ajoutez un nouvel lment au projet. 4 1

En [1], indiquez que vous voulez ajouter une classe. En [2], le nom de la classe. En [3], validez les informations. En [4], le projet [01] a un nouveau fichier source Personne.cs :
1. 2. 3. 4. 5. 6. using using using using System; System.Collections.Generic; System.Linq; System.Text;

namespace ConsoleApplication1 {

Classes, Stuctures, Interfaces

39

7. 8. 9.

class Personne { } }

On modifie l'espace de noms de chacun des fichiers source en Chap2 et on supprime l'importation des espaces de noms inutiles :
1. 2. 3. 4. 5. 6. 1. 2. 3. 4. 5. 6. 7. 8. using System; namespace Chap2 { class Personne { } } using System; namespace Chap2 { class Program { static void Main(string[] args) { } } }

2.1.3
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23.

Dfinition de la classe Personne


using System; namespace Chap2 { public class Personne { // attributs private string prenom; private string nom; private int age; // mthode public void Initialise(string P, string N, int age) { this.prenom = P; this.nom = N; this.age = age; } // mthode public void Identifie() { Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age); }

La dfinition de la classe Personne dans le fichier source [Personne.cs] sera la suivante :

} }

Nous avons ici la dfinition d'une classe, donc d'un type de donnes. Lorsqu'on va crer des variables de ce type, on les appellera des objets ou des instances de classe. Une classe est donc un moule partir duquel sont construits des objets. Les membres ou champs d'une classe peuvent tre des donnes (attributs), des mthodes (fonctions), des proprits. Les proprits sont des mthodes particulires servant connatre ou fixer la valeur d'attributs de l'objet. Ces champs peuvent tre accompagns de l'un des trois mots cls suivants :
priv public protg

Un champ priv (private) n'est accessible que par les seules mthodes internes de la classe Un champ public (public) est accessible par toute mthode dfinie ou non au sein de la classe Un champ protg (protected) n'est accessible que par les seules mthodes internes de la classe ou d'un objet driv (voir ultrieurement le concept d'hritage).

En gnral, les donnes d'une classe sont dclares prives alors que ses mthodes et proprits sont dclares publiques. Cela signifie que l'utilisateur d'un objet (le programmeur)

n'aura pas accs directement aux donnes prives de l'objet pourra faire appel aux mthodes publiques de l'objet et notamment celles qui donneront accs ses donnes prives.

Classes, Stuctures, Interfaces

40

La syntaxe de dclaration d'une classe C est la suivante :


public class C{ private donne ou mthode ou proprit prive; public donne ou mthode ou proprit publique; protected donne ou mthode ou proprit protge; }

L'ordre de dclaration des attributs private, protected et public est quelconque.

2.1.4

La mthode Initialise

Revenons notre classe Personne dclare comme :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. using System; namespace Chap2 { public class Personne { // attributs private string prenom; private string nom; private int age; // mthode public void Initialise(string p, string n, int age) { this.prenom = p; this.nom = n; this.age = age; } // mthode public void Identifie() { Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age); }

} }

Quel est le rle de la mthode Initialise ? Parce que nom, prenom et age sont des donnes prives de la classe Personne, les instructions :
Personne p1; p1.prenom="Jean"; p1.nom="Dupont"; p1.age=30;

sont illgales. Il nous faut initialiser un objet de type Personne via une mthode publique. C'est le rle de la mthode Initialise. On crira :
Personne p1; p1.Initialise("Jean","Dupont",30);

L'criture p1.Initialise est lgale car Initialise est d'accs public.

2.1.5

L'oprateur new

La squence d'instructions
Personne p1; p1.Initialise("Jean","Dupont",30);

est incorrecte. L'instruction


Personne p1;

dclare p1 comme une rfrence un objet de type Personne. Cet objet n'existe pas encore et donc p1 n'est pas initialis. C'est comme si on crivait :
Personne p1=null;

Classes, Stuctures, Interfaces

41

o on indique explicitement avec le mot cl null que la variable p1 ne rfrence encore aucun objet. Lorsqu'on crit ensuite
p1.Initialise("Jean","Dupont",30);

on fait appel la mthode Initialise de l'objet rfrenc par p1. Or cet objet n'existe pas encore et le compilateur signalera l'erreur. Pour que p1 rfrence un objet, il faut crire :
Personne p1=new Personne();

Cela a pour effet de crer un objet de type Personne non encore initialis : les attributs nom et prenom qui sont des rfrences d'objets de type String auront la valeur null, et age la valeur 0. Il y a donc une initialisation par dfaut. Maintenant que p1 rfrence un objet, l'instruction d'initialisation de cet objet
p1.Initialise("Jean","Dupont",30);

est valide.

2.1.6

Le mot cl this

Regardons le code de la mthode initialise :


1. 2. 3. 4. 5. public void Initialise(string p, string n, int age) { this.prenom = p; this.nom = n; this.age = age;

L'instruction this.prenom=p signifie que l'attribut prenom de l'objet courant (this) reoit la valeur p. Le mot cl this dsigne l'objet courant : celui dans lequel se trouve la mthode excute. Comment le connat-on ? Regardons comment se fait l'initialisation de l'objet rfrenc par p1 dans le programme appelant :
p1.Initialise("Jean","Dupont",30);

C'est la mthode Initialise de l'objet p1 qui est appele. Lorsque dans cette mthode, on rfrence l'objet this, on rfrence en fait l'objet p1. La mthode Initialise aurait aussi pu tre crite comme suit :
1. 2. 3. 4. 5. public void Initialise(string p, string n, int age) { prenom = p; nom = n; this.age = age; }

Lorsqu'une mthode d'un objet rfrence un attribut A de cet objet, l'criture this.A est implicite. On doit l'utiliser explicitement lorsqu'il y a conflit d'identificateurs. C'est le cas de l'instruction :
this.age=age;

o age dsigne un attribut de l'objet courant ainsi que le paramtre age reu par la mthode. Il faut alors lever l'ambigut en dsignant l'attribut age par this.age.

2.1.7

Un programme de test

Voici un court programme de test. Celui-ci est crit dans le fichier source [Program.cs] :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. using System; namespace Chap2 { class P01 { static void Main() { Personne p1 = new Personne(); p1.Initialise("Jean", "Dupont", 30); p1.Identifie(); } } }

Classes, Stuctures, Interfaces

42

Avant d'excuter le projet [01], il peut tre ncessaire de prciser le fichier source excuter :

Dans les proprits du projet [01], on indique en [1] la classe excuter. Les rsultats obtenus l'excution sont les suivants :
[Jean, Dupont, 30]

2.1.8

Une autre mthode Initialise

Considrons toujours la classe Personne et rajoutons-lui la mthode suivante :


1. 2. 3. 4. 5. public void Initialise(Personne p) { prenom = p.prenom; nom = p.nom; age = p.age;

On a maintenant deux mthodes portant le nom Initialise : c'est lgal tant qu'elles admettent des paramtres diffrents. C'est le cas ici. Le paramtre est maintenant une rfrence p une personne. Les attributs de la personne p sont alors affects l'objet courant (this). On remarquera que la mthode Initialise a un accs direct aux attributs de l'objet p bien que ceux-ci soient de type private. C'est toujours vrai : un objet o1 d'une classe C a toujours accs aux attributs des objets de la mme classe C. Voici un test de la nouvelle classe Personne :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. using System; namespace Chap2 { class Program { static void Main() { Personne p1 = new Personne(); p1.Initialise("Jean", "Dupont", 30); p1.Identifie(); Personne p2 = new Personne(); p2.Initialise(p1); p2.Identifie(); } } }

et ses rsultats :
1. 2. [Jean, Dupont, 30] [Jean, Dupont, 30]

2.1.9

Constructeurs de la classe Personne

Un constructeur est une mthode qui porte le nom de la classe et qui est appele lors de la cration de l'objet. On s'en sert gnralement pour l'initialiser. C'est une mthode qui peut accepter des arguments mais qui ne rend aucun rsultat. Son prototype ou sa dfinition ne sont prcds d'aucun type (pas mme void). Si une classe C a un constructeur acceptant n arguments argi, la dclaration et l'initialisation d'un objet de cette classe pourra se faire sous la forme :

Classes, Stuctures, Interfaces

43

C objet =new C(arg1,arg2, ... argn); ou C objet; objet=new C(arg1,arg2, ... argn);

Lorsqu'une classe C a un ou plusieurs constructeurs, l'un de ces constructeurs doit tre obligatoirement utilis pour crer un objet de cette classe. Si une classe C n'a aucun constructeur, elle en a un par dfaut qui est le constructeur sans paramtres : public C(). Les attributs de l'objet sont alors initialiss avec des valeurs par dfaut. C'est ce qui s'est pass lorsque dans les programmes prcdents, o on avait crit :
Personne p1; p1=new Personne();

Crons deux constructeurs notre classe Personne :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. using System; namespace Chap2 { public class Personne { // attributs private string prenom; private string nom; private int age; // constructeurs public Personne(String p, String n, int age) { Initialise(p, n, age); } public Personne(Personne P) { Initialise(P); } // mthode public void Initialise(string p, string n, int age) { } ... public void Initialise(Personne p) { } // mthode public void Identifie() { Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age); } } }

...

Nos deux constructeurs se contentent de faire appel aux mthodes Initialise tudies prcdemment. On rappelle que lorsque dans un constructeur, on trouve la notation Initialise(p) par exemple, le compilateur traduit par this.Initialise(p). Dans le constructeur, la mthode Initialise est donc appele pour travailler sur l'objet rfrenc par this, c'est dire l'objet courant, celui qui est en cours de construction. Voici un court programme de test :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. using System; namespace Chap2 { class Program { static void Main() { Personne p1 = new Personne("Jean", "Dupont", 30); p1.Identifie(); Personne p2 = new Personne(p1); p2.Identifie(); } } }

Classes, Stuctures, Interfaces

44

et les rsultats obtenus :


[Jean, Dupont, 30] [Jean, Dupont, 30]

2.1.10

Les rfrences d'objets

Nous utilisons toujours la mme classe Personne. Le programme de test devient le suivant :
1. using System; 2. 3. namespace Chap2 { 4. class Program2 { 5. static void Main() { 6. // p1 7. Personne p1 = new Personne("Jean", "Dupont", 30); 8. Console.Write("p1="); p1.Identifie(); 9. // p2 rfrence le mme objet que p1 10. Personne p2 = p1; 11. Console.Write("p2="); p2.Identifie(); 12. // p3 rfrence un objet qui sera une copie de l'objet rfrenc par p1 13. Personne p3 = new Personne(p1); 14. Console.Write("p3="); p3.Identifie(); 15. // on change l'tat de l'objet rfrenc par p1 16. p1.Initialise("Micheline", "Benot", 67); 17. Console.Write("p1="); p1.Identifie(); 18. // comme p2=p1, l'objet rfrenc par p2 a du changer d'tat 19. Console.Write("p2="); p2.Identifie(); 20. // comme p3 ne rfrence pas le mme objet que p1, l'objet rfrenc par p3 n'a pas du changer 21. Console.Write("p3="); p3.Identifie(); 22. } 23. } 24. }

Les rsultats obtenus sont les suivants :


1. 2. 3. 4. 5. 6. p1=[Jean, Dupont, 30] p2=[Jean, Dupont, 30] p3=[Jean, Dupont, 30] p1=[Micheline, Benot, 67] p2=[Micheline, Benot, 67] p3=[Jean, Dupont, 30]

Lorsqu'on dclare la variable p1 par


Personne p1=new Personne("Jean","Dupont",30);

p1 rfrence l'objet Personne("Jean","Dupont",30) mais n'est pas l'objet lui-mme. En C, on dirait que c'est un pointeur, c.a.d. l'adresse de l'objet cr. Si on crit ensuite :
p1=null;

Ce n'est pas l'objet Personne("Jean","Dupont",30) qui est modifi, c'est la rfrence p1 qui change de valeur. L'objet Personne("Jean","Dupont",30) sera "perdu" s'il n'est rfrenc par aucune autre variable. Lorsqu'on crit :
Personne p2=p1;

on initialise le pointeur p2 : il "pointe" sur le mme objet (il dsigne le mme objet) que le pointeur p1. Ainsi si on modifie l'objet "point" (ou rfrenc) par p1, on modifie aussi celui rfrenc par p2. Lorsqu'on crit :
Personne p3=new Personne(p1);

il y a cration d'un nouvel objet Personne. Ce nouvel objet sera rfrenc par p3. Si on modifie l'objet "point" (ou rfrenc) par p1, on ne modifie en rien celui rfrenc par p3. C'est ce que montrent les rsultats obtenus.

Classes, Stuctures, Interfaces

45

2.1.11

Passage de paramtres de type rfrence d'objet

Dans le chapitre prcdent, nous avons tudi les modes de passage des paramtres d'une fonction lorsque ceux-ci reprsentaient un type C# simple reprsent par une structure .NET. Voyons ce qui se passe lorsque la paramtre est une rfrence d'objet :
1. 2. 3. 4. 5. 6. 7. 8. using System; using System.Text;

namespace Chap1 { class P12 { public static void Main() { // exemple 4 StringBuilder sb0 = new StringBuilder("essai0"), sb1 = new StringBuilder("essai1"), sb2 = new StringBuilder("essai2"), sb3; 9. Console.WriteLine("Dans fonction appelante avant appel : sb0={0}, sb1={1}, sb2={2}", sb0,sb1, sb2); 10. ChangeStringBuilder(sb0, sb1, ref sb2, out sb3); 11. Console.WriteLine("Dans fonction appelante aprs appel : sb0={0}, sb1={1}, sb2={2}, sb3={3}", sb0, sb1, sb2, sb3); 12. 13. } 14. 15. private static void ChangeStringBuilder(StringBuilder sbf0, StringBuilder sbf1, ref StringBuilder sbf2, out StringBuilder sbf3) { 16. Console.WriteLine("Dbut fonction appele : sbf0={0}, sbf1={1}, sbf2={2}", sbf0,sbf1, sbf2); 17. sbf0.Append("*****"); 18. sbf1 = new StringBuilder("essai1*****"); 19. sbf2 = new StringBuilder("essai2*****"); 20. sbf3 = new StringBuilder("essai3*****"); 21. Console.WriteLine("Fin fonction appele : sbf0={0}, sbf1={1}, sbf2={2}, sbf3={3}", sbf0, sbf1, sbf2, sbf3); 22. } 23. } 24. }

ligne 8 : dfinit 3 objets de type StringBuilder. Un objet StringBuilder est proche d'un objet string. Lorsqu'on manipule un objet string, on obtient en retour un nouvel objet string. Ainsi dans la squence de code :
string s="une chane"; s=s.ToUpperCase();

1. 2.

La ligne 1 cre un objet string en mmoire et s est son adresse. Ligne 2, s.ToUpperCase() cre un autre objet string en mmoire. Ainsi entre les lignes 1 et 2, s a chang de valeur (il pointe sur le nouvel objet). La classe StringBuilder elle, permet de transformer une chane sans qu'un second objet soit cr. C'est l'exemple donn plus haut :

ligne 8 : 4 rfrences [sb0, sb1, sb2, sb3] des objets de type StringBuilder ligne 10 : sont passes la mthode ChangeStringBuilder avec des modes diffrents : sb0, sb1 avec le mode par dfaut, sb2 avec le mot cl ref, sb3 avec le mot cl out. lignes 15-22 : une mthode qui a les paramtres formels [sbf0, sbf1, sbf2, sbf3]. Les relations entre paramtres formels formels sbfi et effectifs sbi sont les suivantes : sbf0 et sb0 sont, au dmarrage de la mthode, deux rfrences distinctes qui pointent sur le mme objet (passage par valeur des adresses) idem pour sbf1 et sb1 sbf2 et sb2 sont, au dmarrage de la mthode, une mme rfrence sur le mme objet (mot cl ref) sbf3 et sb3 sont, aprs excution de la mthode, une mme rfrence sur le mme objet (mot cl out)

Les rsultats obtenus sont les suivants :


1. 2. 3. 4. Dans fonction appelante avant appel : sb0=essai0, sb1=essai1, sb2=essai2 Dbut fonction appele : sbf0=essai0, sbf1=essai1, sbf2=essai2 Fin fonction appele : sbf0=essai0*****, sbf1=essai1*****, sbf2=essai2*****, sbf3=essai3***** Dans fonction appelante aprs appel : sb0=essai0*****, sb1=essai1, sb2=essai2*****, sb3=essai3*****

Explications :

sb0 et sbf0 sont deux rfrences distinctes sur le mme objet. Celui-ci a t modifi via sbf0 - ligne 3. Cette modification peut tre vue via sb0 - ligne 4.

Classes, Stuctures, Interfaces

46

sb1 et sbf1 sont deux rfrences distinctes sur le mme objet. sbf1 voit sa valeur modifie dans la mthode et pointe dsormais sur un nouvel objet - ligne 3. Cela ne change en rien la valeur de sb1 qui continue pointer sur le mme objet ligne 4. sb2 et sbf2 sont une mme rfrence sur le mme objet. sbf2 voit sa valeur modifie dans la mthode et pointe dsormais sur un nouvel objet - ligne 3. Comme sbf2 et sb2 sont une seule et mme entit, la valeur de sb2 a t galement modifie et sb2 pointe sur le mme objet que sbf2 - lignes 3 et 4. avant appel de la mthode, sb3 n'avait pas de valeur. Aprs la mthode, sb3 reoit la valeur de sbf3. On a donc deux rfrences sur le mme objet - lignes 3 et 4

2.1.12

Les objets temporaires

Dans une expression, on peut faire appel explicitement au constructeur d'un objet : celui-ci est construit, mais nous n'y avons pas accs (pour le modifier par exemple). Cet objet temporaire est construit pour les besoins d'valuation de l'expression puis abandonn. L'espace mmoire qu'il occupait sera automatiquement rcupr ultrieurement par un programme appel "ramassemiettes" dont le rle est de rcuprer l'espace mmoire occup par des objets qui ne sont plus rfrencs par des donnes du programme. Considrons le nouveau programme de test suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. using System; namespace Chap2 { class Program { static void Main() { new Personne(new Personne("Jean", "Dupont", 30)).Identifie(); } } }

et modifions les constructeurs de la classe Personne afin qu'ils affichent un message :


1. 2. 3. 4. 5. 6. 7. 8. 9. // constructeurs public Personne(String p, String n, int age) { Console.WriteLine("Constructeur Personne(string, string, int)"); Initialise(p, n, age); } public Personne(Personne P) { Console.Out.WriteLine("Constructeur Personne(Personne)"); Initialise(P); }

Nous obtenons les rsultats suivants :


1. 2. 3. Constructeur Personne(string, string, int) Constructeur Personne(Personne) [Jean, Dupont, 30]

montrant la construction successive des deux objets temporaires.

2.1.13

Mthodes de lecture et d'criture des attributs privs

Nous rajoutons la classe Personne les mthodes ncessaires pour lire ou modifier l'tat des attributs des objets :
1. using System; 2. 3. namespace Chap2 { 4. public class Personne { 5. // attributs 6. private string prenom; 7. private string nom; 8. private int age; 9. 10. // constructeurs 11. public Personne(String p, String n, int age) { 12. Console.WriteLine("Constructeur Personne(string, string, int)"); 13. Initialise(p, n, age); 14. } 15. public Personne(Personne p) {

Classes, Stuctures, Interfaces

47

16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. } 60. 61. }

Console.Out.WriteLine("Constructeur Personne(Personne)"); Initialise(p);

// mthode public void Initialise(string p, string n, int age) { this.prenom = p; this.nom = n; this.age = age; } public void Initialise(Personne p) { prenom = p.prenom; nom = p.nom; age = p.age; } // accesseurs public String GetPrenom() { return prenom; } public String GetNom() { return nom; } public int GetAge() { return age; } //modifieurs public void SetPrenom(String P) { this.prenom = P; } public void SetNom(String N) { this.nom = N; } public void SetAge(int age) { this.age = age; } // mthode public void Identifie() { Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age); }

Nous testons la nouvelle classe avec le programme suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. using System; namespace Chap2 { class Program { static void Main(string[] args) { Personne p = new Personne("Jean", "Michelin", 34); Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")"); p.SetAge(56); Console.Out.WriteLine("p=(" + p.GetPrenom() + "," + p.GetNom() + "," + p.GetAge() + ")"); } } }

et nous obtenons les rsultats :


1. 2. 3. Constructeur Personne(string, string, int) p=(Jean,Michelin,34) p=(Jean,Michelin,56)

2.1.14

Les proprits

Il existe une autre faon d'avoir accs aux attributs d'une classe, c'est de crer des proprits. Celles-ci nous permettent de manipuler des attributs privs comme s'ils taient publics.

Classes, Stuctures, Interfaces

48

Considrons la classe Personne suivante o les accesseurs et modifieurs prcdents ont t remplacs par des proprits en lecture et criture :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. using System; namespace Chap2 { public class Personne { // attributs private string prenom; private string nom; private int age; // constructeurs public Personne(String p, String n, int age) { Initialise(p, n, age); } public Personne(Personne p) { Initialise(p); } // mthode public void Initialise(string p, string n, int age) { this.prenom = p; this.nom = n; this.age = age; } public void Initialise(Personne p) { prenom = p.prenom; nom = p.nom; age = p.age; } // proprits public string Prenom { get { return prenom; } set { // prnom valide ? if (value == null || value.Trim().Length == 0) { throw new Exception("prnom (" + value + ") invalide"); } else { prenom = value; } }//if }//prenom public string Nom { get { return nom; } set { // nom valide ? if (value == null || value.Trim().Length == 0) { throw new Exception("nom (" + value + ") invalide"); } else { nom = value; } }//if }//nom public int Age { get { return age; } set { // age valide ? if (value >= 0) { age = value; } else throw new Exception("ge (" + value + ") invalide"); }//if }//age // mthode public void Identifie() { Console.WriteLine("[{0}, {1}, {2}]", prenom, nom, age); }

} }

Classes, Stuctures, Interfaces

49

Une proprit permet de lire (get) ou de fixer (set) la valeur d'un attribut. Une proprit est dclare comme suit : public Type Proprit{ get {...} set {...} } o Type doit tre le type de l'attribut gr par la proprit. Elle peut avoir deux mthodes appeles get et set. La mthode get est habituellement charge de rendre la valeur de l'attribut qu'elle gre (elle pourrait rendre autre chose, rien ne l'empche). La mthode set reoit un paramtre appel value qu'elle affecte normalement l'attribut qu'elle gre. Elle peut en profiter pour faire des vrifications sur la validit de la valeur reue et ventuellement lancer un exception si la valeur se rvle invalide. C'est ce qui est fait ici. Comment ces mthodes get et set sont-elles appeles ? Considrons le programme de test suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. using System; namespace Chap2 { class Program { static void Main(string[] args) { Personne p = new Personne("Jean", "Michelin", 34); Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")"); p.Age = 56; Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")"); try { p.Age = -4; } catch (Exception ex) { Console.Error.WriteLine(ex.Message); }//try-catch } } }

Dans l'instruction
Console.Out.WriteLine("p=(" + p.Prenom + "," + p.Nom + "," + p.Age + ")");

on cherche avoir les valeurs des proprits Prenom, Nom et Age de la personne p. C'est la mthode get de ces proprits qui est alors appele et qui rend la valeur de l'attribut qu'elles grent. Dans l'instruction on veut fixer la valeur de la proprit Age. C'est alors la mthode set de cette proprit qui est alors appele. Elle recevra 56 dans son paramtre value. Une proprit P d'une classe C qui ne dfinirait que la mthode get est dite en lecture seule. Si c est un objet de classe C, l'opration c.P=valeur sera alors refuse par le compilateur. L'excution du programme de test prcdent donne les rsultats suivants :
1. 2. 3. p=(Jean,Michelin,34) p=(Jean,Michelin,56) ge (-4) invalide p.Age=56;

Les proprits nous permettent donc de manipuler des attributs privs comme s'ils taient publics. Une autre caractristique des proprits est qu'elles peuvent tre utilises conjointement avec un constructeur selon la syntaxe suivante :
Classe objet=new Classe (...) {Proprit1=val1, Proprit2=val2, ...}

Cette syntaxe est quivalente au code suivant :


1. 2. 3. 4. Classe objet=new Classe(...); objet.Proprit1=val1; objet.Proprit2=val2; ...

L'ordre des proprits n'importe pas. Voici un exemple.

Classes, Stuctures, Interfaces

50

La classe Personne se voit ajouter un nouveau constructeur sans paramtres :


1. 2. } public Personne() {

Le constructeur n'initialise pas les membres de l'objet. C'est ce qu'on appelle le constructeur par dfaut. C'est lui qui est utilis lorsque la classe ne dfinit aucun constructeur. Le code suivant cre et initialise (ligne 6) une nouvelle Personne avec la syntaxe prsente prcdemment :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using System; namespace Chap2 { class Program { static void Main(string[] args) { Personne p2 = new Personne { Age = 7, Prenom = "Arthur", Nom = "Martin" }; Console.WriteLine("p2=({0},{1},{2})", p2.Prenom, p2.Nom, p2.Age); } } }

Ligne 6 ci-dessus, c'est le constructeur sans paramtres Personne() qui est utilis. Dans ce cas particulier, on aurait pu aussi crire
Personne p2 = new Personne() { Age = 7, Prenom = "Arthur", Nom = "Martin" };

mais les parenthses du constructeur Personne() sans paramtres ne sont pas obligatoires dans cette syntaxe. Les rsultats de l'excution sont les suivants :
p2=(Arthur,Martin,7)

Dans beaucoup de cas, les mthodes get et set d'une proprit se contentent de lire et crire un champ priv sans autre traitement. On peut alors, dans ce scnario, utiliser une proprit automatique dclare comme suit : public Type Proprit{ get ; set ; } Le champ priv associ la proprit n'est pas dclar. Il est automatiquement gnr par le compilateur. On y accde que via sa proprit. Ainsi, au lieu d'crire :
private string prenom; ... // proprit associe public string Prenom { get { return prenom; } set { // prnom valide ? if (value == null || value.Trim().Length == 0) { throw new Exception("prnom (" + value + ") invalide"); } else { prenom = value; } }//if }//prenom

on pourra crire :
public string Prenom {get; set;}

sans dclarer le champ priv prenom. La diffrence entre les deux proprits prcdentes est que la premire vrifie la validit du prnom dans le set, alors que la deuxime ne fait aucune vrification. Utiliser la proprit automatique Prenom revient dclarer un champ Prenom public :
public string Prenom;

On peut se demander s'il y a une diffrence entre les deux dclarations. Dclarer public un champ d'une classe est dconseill. Cela rompt avec le concept d'encapsulation de l'tat d'un objet, tat qui doit tre priv et expos par des mthodes publiques.

Classes, Stuctures, Interfaces

51

Si la proprit automatique est dclare virtuelle, elle peut alors tre redfinie dans une classe fille :
1. 2. 3. 1. 2. 3. class Class1 { public virtual string Prop { get; set; } } class Class2 : Class1 { public override string Prop { get { return base.Prop; } set {... } } }

Ligne 2 ci-dessus, la classe fille Class2 peut mettre dans le set, du code vrifiant la validit de la valeur affecte la proprit automatique base.Prop de la classe mre Class1.

2.1.15

Les mthodes et attributs de classe

Supposons qu'on veuille compter le nombre d'objets Personne cres dans une application. On peut soi-mme grer un compteur mais on risque d'oublier les objets temporaires qui sont crs ici ou l. Il semblerait plus sr d'inclure dans les constructeurs de la classe Personne, une instruction incrmentant un compteur. Le problme est de passer une rfrence de ce compteur afin que le constructeur puisse l'incrmenter : il faut leur passer un nouveau paramtre. On peut aussi inclure le compteur dans la dfinition de la classe. Comme c'est un attribut de la classe elle-mme et non celui d'une instance particulire de cette classe, on le dclare diffremment avec le mot cl static :
private static long nbPersonnes;

Pour le rfrencer, on crit Personne.nbPersonnes pour montrer que c'est un attribut de la classe Personne elle-mme. Ici, nous avons cr un attribut priv auquel on n'aura pas accs directement en-dehors de la classe. On cre donc une proprit publique pour donner accs l'attribut de classe nbPersonnes. Pour rendre la valeur de nbPersonnes la mthode get de cette proprit n'a pas besoin d'un objet Personne particulier : en effet nbPersonnes est l'attribut de toute une classe. Aussi a-t-on besoin d'une proprit dclare elleaussi static :
1. 2. 3. public static long NbPersonnes { get { return nbPersonnes; }

qui de l'extrieur sera appele avec la syntaxe Personne.NbPersonnes. Voici un exemple. La classe Personne devient la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; namespace Chap2 { public class Personne { // attributs de classe private static long nbPersonnes; public static long NbPersonnes { get { return nbPersonnes; } } // attributs d'instance private string prenom; private string nom; private int age; // constructeurs public Personne(String p, String n, int age) { Initialise(p, n, age); nbPersonnes++; } public Personne(Personne p) { Initialise(p); nbPersonnes++; } ... }

Classes, Stuctures, Interfaces

52

Lignes 20 et 24, les constructeurs incrmentent le champ statique de la ligne 7. Avec le programme suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. using System; namespace Chap2 { class Program { static void Main(string[] args) { Personne p1 = new Personne("Jean", "Dupont", 30); Personne p2 = new Personne(p1); new Personne(p1); Console.WriteLine("Nombre de personnes cres : " + Personne.NbPersonnes); } } }

on obtient les rsultats suivants :


Nombre de personnes cres : 3

2.1.16

Un tableau de personnes

Un objet est une donne comme une autre et ce titre plusieurs objets peuvent tre rassembls dans un tableau :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. using System; namespace Chap2 { class Program { static void Main(string[] args) { // un tableau de personnes Personne[] amis = new Personne[3]; amis[0] = new Personne("Jean", "Dupont", 30); amis[1] = new Personne("Sylvie", "Vartan", 52); amis[2] = new Personne("Neil", "Armstrong", 66); // affichage foreach (Personne ami in amis) { ami.Identifie(); } } } }

ligne 7 : cre un tableau de 3 lments de type Personne. Ces 3 lments sont initialiss ici avec la valeur null, c.a.d. qu'ils ne rfrencent aucun objet. De nouveau, par abus de langage, on parle de tableau d'objets alors que ce n'est qu'un tableau de rfrences d'objets. La cration du tableau d'objets, qui est un objet lui-mme (prsence de new) ne cre aucun objet du type de ses lments : il faut le faire ensuite. lignes 8-10 : cration des 3 objets de type Personne lignes 12-14 : affichage du contenu du tableau amis

On obtient les rsultats suivants :


1. 2. 3. [Jean, Dupont, 30] [Sylvie, Vartan, 52] [Neil, Armstrong, 66]

2.2
2.2.1

L'hritage par l'exemple


Gnralits

Nous abordons ici la notion d'hritage. Le but de l'hritage est de "personnaliser" une classe existante pour qu'elle satisfasse nos besoins. Supposons qu'on veuille crer une classe Enseignant : un enseignant est une personne particulire. Il a des attributs qu'une autre personne n'aura pas : la matire qu'il enseigne par exemple. Mais il a aussi les attributs de toute personne : prnom, nom et ge. Un enseignant fait donc pleinement partie de la classe Personne mais a des attributs supplmentaires. Plutt que d'crire une

Classes, Stuctures, Interfaces

53

classe Enseignant partir de rien, on prfrerait reprendre l'acquis de la classe Personne qu'on adapterait au caractre particulier des enseignants. C'est le concept d'hritage qui nous permet cela. Pour exprimer que la classe Enseignant hrite des proprits de la classe Personne, on crira :
public class Enseignant : Personne

Personne est appele la classe parent (ou mre) et Enseignant la classe drive (ou fille). Un objet Enseignant a toutes les qualits d'un objet Personne : il a les mmes attributs et les mmes mthodes. Ces attributs et mthodes de la classe parent ne sont pas rptes dans la dfinition de la classe fille : on se contente d'indiquer les attributs et mthodes rajouts par la classe fille : Nous supposons que la classe Personne est dfinie comme suit :
1. using System; 2. 3. namespace Chap2 { 4. public class Personne { 5. 6. // attributs de classe 7. private static long nbPersonnes; 8. public static long NbPersonnes { 9. get { return nbPersonnes; } 10. } 11. 12. // attributs d'instance 13. private string prenom; 14. private string nom; 15. private int age; 16. 17. // constructeurs 18. public Personne(String prenom, String nom, int age) { 19. Nom = nom; 20. Prenom = prenom; 21. Age = age; 22. nbPersonnes++; 23. Console.WriteLine("Constructeur Personne(string, string, int)"); 24. } 25. public Personne(Personne p) { 26. Nom = p.Nom; 27. Prenom = p.Prenom; 28. Age = p.Age; 29. nbPersonnes++; 30. Console.WriteLine("Constructeur Personne(Personne)"); 31. } 32. 33. // proprits 34. public string Prenom { 35. get { return prenom; } 36. set { 37. // prnom valide ? 38. if (value == null || value.Trim().Length == 0) { 39. throw new Exception("prnom (" + value + ") invalide"); 40. } else { 41. prenom = value; 42. } 43. }//if 44. }//prenom 45. 46. public string Nom { 47. get { return nom; } 48. set { 49. // nom valide ? 50. if (value == null || value.Trim().Length == 0) { 51. throw new Exception("nom (" + value + ") invalide"); 52. } else { nom = value; } 53. }//if 54. }//nom 55. 56. public int Age { 57. get { return age; } 58. set { 59. // age valide ? 60. if (value >= 0) { 61. age = value;

Classes, Stuctures, Interfaces

54

62. 63. 64. 65. 66. 67. 68. 69. 70. 71. } 72. 73. }

} else throw new Exception("ge (" + value + ") invalide"); }//if }//age // proprit public string Identite { get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age);} }

La mthode Identifie a t remplace par la proprit Identite en lecture seule et qui identifie la personne. Nous crons une classe Enseignant hritant de la classe Personne :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. using System; namespace Chap2 { class Enseignant : Personne { // attributs private int section; // constructeur public Enseignant(string prenom, string nom, int age, int section) : base(prenom, nom, age) { // on mmorise la section via la proprit Section Section = section; // suivi Console.WriteLine("Construction Enseignant(string, string, int, int)"); }//constructeur // proprit Section public int Section { get { return section; } set { section = value; } }// Section } }

La classe Enseignant rajoute aux mthodes et attributs de la classe Personne : ligne 4 : la classe Enseignant drive de la classe Personne ligne 6 : un attribut section qui est le n de section auquel appartient l'enseignant dans le corps des enseignants (une section par discipline en gros). Cet attribut priv est accessible via la proprit publique Section des lignes 18-21 ligne 9 : un nouveau constructeur permettant d'initialiser tous les attributs d'un enseignant

2.2.2

Construction d'un objet Enseignant

Une classe fille n'hrite pas des constructeurs de sa classe Parent. Elle doit alors dfinir ses propres constructeurs. Le constructeur de la classe Enseignant est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. // constructeur public Enseignant(string prenom, string nom, int age, int section) : base(prenom, nom, age) { // on mmorise la section Section = section; // suivi Console.WriteLine("Construction enseignant(string, string, int, int)"); }//constructeur

La dclaration
public Enseignant(string prenom, string nom, int age, int section) : base(prenom, nom, age) {

dclare que le constructeur reoit quatre paramtres prenom, nom, age, section et en passe trois (prenom,nom,age) sa classe de base, ici la classe Personne. On sait que cette classe a un constructeur Personne(string, string, int) qui va permettre de construire une personne avec

Classes, Stuctures, Interfaces

55

les paramtres passss (prenom,nom,age). Une fois la construction de la classe de base termine, la construction de l'objet Enseignant se poursuit par l'excution du corps du constructeur :
// on mmorise la section Section = section;

On notera qu' gauche du signe =, ce n'est pas l'attribut section de l'objet qui a t utilis, mais la proprit Section qui lui est associe. Cela permet au constructeur de profiter des ventuels contrles de validit que pourrait faire cette mthode. Cela vite de placer ceux-ci deux endroits diffrents : le constructeur et la proprit. En rsum, le constructeur d'une classe drive : passe sa classe de base les paramtres dont celle-ci a besoin pour se construire utilise les autres paramtres pour initialiser les attributs qui lui sont propres On aurait pu prfrer crire :
// constructeur public Enseignant(string prenom, string nom, int age, int section){ this.prenom=prenom; this.nom=nom; this.age=age; this.section=section; }

C'est impossible. La classe Personne a dclar privs (private) ses trois champs prenom, nom et age. Seuls des objets de la mme classe ont un accs direct ces champs. Tous les autres objets, y compris des objets fils comme ici, doivent passer par des mthodes publiques pour y avoir accs. Cela aurait t diffrent si la classe Personne avait dclar protgs (protected) les trois champs : elle autorisait alors des classes drives avoir un accs direct aux trois champs. Dans notre exemple, utiliser le constructeur de la classe parent tait donc la bonne solution et c'est la mthode habituelle : lors de la construction d'un objet fils, on appelle d'abord le constructeur de l'objet parent puis on complte les initialisations propres cette fois l'objet fils (section dans notre exemple). Tentons un premier programme de test [Program.cs] :
1. 2. 3. 4. 5. 6. 7. 8. 9. using System; namespace Chap2 { class Program { static void Main(string[] args) { Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite); } } }

Ce programme ce contente de crer un objet Enseignant (new) et de l'identifier. La classe Enseignant n'a pas de mthode Identite mais sa classe parent en a une qui de plus est publique : elle devient par hritage une mthode publique de la classe Enseignant. L'ensemble du projet est le suivant :

Personne.cs : la classe Personne Enseignant.cs : la classe Enseignant Program.cs : le programme de test

Les rsultats obtenus sont les suivants :


1. 2. 3. Constructeur Personne(string, string, int) Construction Enseignant(string, string, int, int) [Jean, Dupont, 30]

On voit que : un objet Personne (ligne 1) a t construit avant l'objet Enseignant (ligne 2) l'identit obtenue est celle de l'objet Personne

Classes, Stuctures, Interfaces

56

2.2.3

Redfinition d'une mthode ou d'une proprit

Dans l'exemple prcdent, nous avons eu l'identit de la partie Personne de l'enseignant mais il manque certaines informations propres la classe Enseignant (la section). On est donc amen crire une proprit permettant d'identifier l'enseignant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; namespace Chap2 { class Enseignant : Personne { // attributs private int section; // constructeur public Enseignant(string prenom, string nom, int age, int section) : base(prenom, nom, age) { // on mmorise la section via la proprit Section Section = section; // suivi Console.WriteLine("Construction Enseignant(string, string, int, int)"); }//constructeur // proprit Section public int Section { get { return section; } set { section = value; } }// section // proprit Identite public new string Identite { get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); } }

} }

Lignes 24-26, la proprit Identite de la classe Enseignant s'appuie sur la proprit Identite de sa classe mre (base.Identite) (ligne 25) pour afficher sa partie "Personne" puis complte avec le champ section qui est propre la classe Enseignant. Notons la dclaration de la proprit Identite :
public new string Identite{

Soit un objet enseignant E. Cet objet contient en son sein un objet Personne : E Enseignant Personne Identite Identite La proprit Identite est dfinie la fois dans la classe Enseignant et sa classe mre Personne. Dans la classe fille Enseignant, la proprit Identite doit tre prcde du mot cl new pour indiquer qu'on redfinit une nouvelle proprit Identite pour la classe Enseignant.
public new string Identite{

La classe Enseignant dispose maintenant de deux proprits Identite : celle hrite de la classe parent Personne la sienne propre Si E est un ojet Enseignant, E.Identite dsigne la proprit Identite de la classe Enseignant. On dit que la proprit Identite de la classe fille redfinit ou cache la proprit Identite de la classe mre. De faon gnrale, si O est un objet et M une mthode, pour excuter la mthode O.M, le systme cherche une mthode M dans l'ordre suivant : dans la classe de l'objet O dans sa classe mre s'il en a une dans la classe mre de sa classe mre si elle existe

Classes, Stuctures, Interfaces

57

etc

L'hritage permet donc de redfinir dans la classe fille des mthodes/proprits de mme nom dans la classe mre. C'est ce qui permet d'adapter la classe fille ses propres besoins. Associe au polymorphisme que nous allons voir un peu plus loin, la redfinition de mthodes/proprits est le principal intrt de l'hritage. Considrons le mme programme de test que prcdemment :
1. 2. 3. 4. 5. 6. 7. 8. 9. using System; namespace Chap2 { class Program { static void Main(string[] args) { Console.WriteLine(new Enseignant("Jean", "Dupont", 30, 27).Identite); } } }

Les rsultats obtenus sont cette fois les suivants :


1. 2. 3. Constructeur Personne(string, string, int) Construction Enseignant(string, string, int, int) Enseignant[[Jean, Dupont, 30],27]

2.2.4

Le polymorphisme

Considrons une ligne de classes : C0 C1 C2 Cn o Ci Cj indique que la classe Cj est drive de la classe Ci. Cela entrane que la classe Cj a toutes les caractristiques de la classe Ci plus d'autres. Soient des objets Oi de type Ci. Il est lgal d'crire : Oi=Oj avec j>i En effet, par hritage, la classe Cj a toutes les caractristiques de la classe C i plus d'autres. Donc un objet Oj de type Cj contient en lui un objet de type Ci. L'opration Oi=Oj fait que Oi est une rfrence l'objet de type Ci contenu dans l'objet Oj. Le fait qu'une variable Oi de classe Ci puisse en fait rfrencer non seulement un objet de la classe Ci mais en fait tout objet driv de la classe Ci, est appel polymorphisme : la facult pour une variable de rfrencer diffrents types d'objets. Prenons un exemple et considrons la fonction suivante indpendante de toute classe (static):
public static void Affiche(Personne p){ . }

On pourra aussi bien crire


Personne p; ... Affiche(p);

que
Enseignant e; ... Affiche(e);

Dans ce dernier cas, le paramtre formel p de type Personne de la mthode statique Affiche va recevoir une valeur de type Enseignant. Comme le type Enseignant drive du type Personne, c'est lgal.

Classes, Stuctures, Interfaces

58

2.2.5

Redfinition et polymorphisme

Compltons notre mthode Affiche :


1. 2. 3. 4. public static void Affiche(Personne p) { // affiche identit de p Console.WriteLine(p.Identite); }//affiche

La proprit p.Identite rend une chane de caractres identifiant l'objet Personne p. Que se passe-t-il dans l'exemple prcdent si le paramtre pass la mthode Affiche est un objet de type Enseignant :
Enseignant e = new Enseignant(...); Affiche(e);

Regardons l'exemple suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. using System; namespace Chap2 { class Program2 { static void Main(string[] args) { // un enseignant Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61); Affiche(e); // une personne Personne p = new Personne("Jean", "Dupont", 30); Affiche(p); } // affiche public static void Affiche(Personne p) { // affiche identit de p Console.WriteLine(p.Identite); }//affiche

} }

Les rsultats obtenus sont les suivants :


1. 2. 3. 4. 5. Constructeur Personne(string, string, int) Construction Enseignant(string, string, int, int) [Lucile, Dumas, 56] Constructeur Personne(string, string, int) [Jean, Dupont, 30]

L'excution montre que l'instruction p.Identite (ligne 17) a excut chaque fois la proprit Identite d'une Personne, d'abord (ligne 7) la personne contenue dans l'Enseignant e, puis (ligne 10) la Personne p elle-mme. Elle ne s'est pas adapte l'objet rellement pass en paramtre Affiche. On aurait prfr avoir l'identit complte de l'Enseignant e. Il aurait fallu pour cela que la notation p.Identite rfrence la proprit Identite de l'objet rellement point par p plutt que la proprit Identite de partie "Personne" de l'objet rellement par p. Il est possible d'obtenir ce rsultat en dclarant Identite comme une proprit virtuelle (virtual) dans la classe de base Personne :
1. 2. 3. public virtual string Identite { get { return String.Format("[{0}, {1}, {2}]", prenom, nom, age); } }

Le mot cl virtual fait de Identite une proprit virtuelle. Ce mot cl peut s'appliquer galement aux mthodes. Les classes filles qui redfinissent une proprit ou mthode virtuelle doivent alors utiliser le mot cl override au lieu de new pour qualifier leur proprit/mthode redfinie. Ainsi dans la classe Enseignant, la proprit Identite est redfinie comme suit :
1. 2. 3. public override string Identite { get { return String.Format("Enseignant[{0},{1}]", base.Identite, Section); } }

Le programme prcdent produit alors les rsultats suivants :


1. Constructeur Personne(string, string, int)

Classes, Stuctures, Interfaces

59

2. 3. 4. 5.

Construction Enseignant(string, string, int, int) Enseignant[[Lucile, Dumas, 56],61] Constructeur Personne(string, string, int) [Jean, Dupont, 30]

Cette fois-ci, ligne 3, on a bien eu l'identit complte de l'enseignant. Redfinissons maintenant une mthode plutt qu'une proprit. La classe object (alias C# de System.Object) est la classe "mre" de toutes les classes C#. Ainsi lorsqu'on crit :
public class Personne

on crit implicitement :
public class Personne : System.Object

La classe System.Object dfinit une mthode virtuelle ToString :

La mthode ToString rend le nom de la classe laquelle appartient l'objet comme le montre l'exemple suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. using System; namespace Chap2 { class Program2 { static void Main(string[] args) { // un enseignant Console.WriteLine(new Enseignant("Lucile", "Dumas", 56, 61).ToString()); // une personne Console.WriteLine(new Personne("Jean", "Dupont", 30).ToString()); } } }

Les rsultats produits sont les suivants :


1. 2. 3. 4. 5. Constructeur Personne(string, string, int) Construction Enseignant(string, string, int, int) Chap2.Enseignant Constructeur Personne(string, string, int) Chap2.Personne

On remarquera que bien que nous n'ayons pas redfini la mthode ToString dans les classes Personne et Enseignant, on peut cependant constater que la mthode ToString de la classe Object a t capable d'afficher le nom rel de la classe de l'objet. Redfinissons la mthode ToString dans les classes Personne et Enseignant :
1. 2. 3. 4. // mthode ToString public override string ToString() { return Identite; }

Classes, Stuctures, Interfaces

60

La dfinition est la mme dans les deux classes. Considrons le programme de test suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. using System; namespace Chap2 { class Program3 { public static void Main() { // un enseignant Enseignant e = new Enseignant("Lucile", "Dumas", 56, 61); Affiche(e); // une personne Personne p = new Personne("Jean", "Dupont", 30); Affiche(p); } // affiche public static void Affiche(Personne p) { // affiche identit de p Console.WriteLine(p); }//Affiche } }

Attardons-nous sur la mthode Affiche qui admet pour paramtre une personne p. Ligne 15, la mthode WriteLine de la classe Console n'a aucune variante admettant un paramtre de type Personne. Parmi les diffrentes variantes de Writeline, il en existe une qui admet comme paramtre un type Object. Le compilateur va utiliser cette mthode, WriteLine(Object o), parce que cette signature signifie que le paramtre o peut tre de type Object ou driv. Puisque Object est la classe mre de toutes les classes, tout objet peut tre pass en paramtre WriteLine et donc un objet de type Personne ou Enseignant. La mthode WriteLine(Object o) crit o.ToString() dans le flux d'criture Out. La mthode ToString tant virtuelle, si l'objet o (de type Object ou driv) a redfini la mthode ToString, ce sera cette dernire qui sera utilise. C'est ici le cas avec les classes Personne et Enseignant. C'est ce que montrent les rsultats d'excution :
1. 2. 3. 4. 5. Constructeur Personne(string, string, int) Construction Enseignant(string, string, int, int) Enseignant[[Lucile, Dumas, 56],61] Constructeur Personne(string, string, int) [Jean, Dupont, 30]

2.3
2.3.1

Redfir la signification d'un oprateur pour une classe


Introduction

Considrons l'instruction op1 + op2 o op1 et op2 sont deux oprandes. Il est possible de redfinir la signification de l'oprateur + . Si l'oprande op1 est un objet de classe C1, il faut dfinir une mthode statique dans la classe C1 avec la signature suivante :
public static [type] operator +(C1 oprande1, C2 oprande2);

Lorsque le compilateur rencontre l'instruction op1 + op2 il la traduit alors par C1.operator+(op1,op2). Le type rendu par la mthode operator est important. En effet, considrons l'opration op1+op2+op3. Elle est traduite par le compilateur par (op1+op2)+op3. Soit res12 le rsultat de op1+op2. L'opration qui est faite ensuite est res12+op3. Si res12 est de type C1, elle sera traduite elle aussi par C1.operator+(res12,op3). Cela permet d'enchaner les oprations. On peut redfinir galement les oprateurs unaires n'ayant qu'un seul oprande. Ainsi si op1 est un objet de type C1, l'opration op1++ peut tre redfinie par une mthode statique de la classe C1 :
public static [type] operator ++(C1 oprande1);

Ce qui a t dit ici est vrai pour la plupart des oprateurs avec cependant quelques exceptions : les oprateurs == et != doivent tre redfinis en mme temps les oprateurs && ,||, [], (), +=, -=, ... ne peuvent tre redfinis

Classes, Stuctures, Interfaces

61

2.3.2

Un exemple

On cre une classe ListeDePersonnes drive de la classe ArrayList. Cette classe implmente une liste dynamique et est prsente dans le chapitre qui suit. De cette classe, nous n'utilisons que les lments suivants : la mthode L.Add(Object o) permettant d'ajouter la liste L un objet o. Ici l'objet o sera un objet Personne. la proprit L.Count qui donne le nombre d'lments de la liste L la notation L[i] qui donne l'lment i de la liste L La classe ListeDePersonnes va hriter de tous les attributs, mthodes et proprits de la classe ArrayList. Sa dfinition est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. using System; using System.Collections; using System.Text; namespace Chap2 { class ListeDePersonnes : ArrayList{ // redfinition oprateur +, pour ajouter une personne la liste public static ListeDePersonnes operator +(ListeDePersonnes l, Personne p) { // on ajoute la Personne p la ListeDePersonnes l l.Add(p); // on rend la ListeDePersonnes l return l; }// operator + // ToString public override string ToString() { // rend (l1, l2, ..., ln) // parenthse ouvrante StringBuilder listeToString = new StringBuilder("("); // on parcourt la liste de personnes (this) for (int i = 0; i < Count - 1; i++) { listeToString.Append(this[i]).Append(","); }//for // dernier lment if (Count != 0) { listeToString.Append(this[Count-1]); } // parenthse fermante listeToString.Append(")"); // on doit rendre un string return listeToString.ToString(); }//ToString

} }

ligne 6 : la classe ListeDePersonnes drive de la classe ArrayList lignes 8-13 : dfinition de l'oprateur + pour l'opration l + p, o l est de type ListeDePersonnes et p de type Personne ou driv. ligne 10 : la personne p est ajoute la liste l. C'est la mthode Add de la classe parent ArrayList qui est ici utilise. ligne 12 : la rfrence sur la liste l est rendue afin de pouvoir enchaner les oprateurs + tels que dans l + p1 + p2. L'opration l+p1+p2 sera interprte (priorit des oprateurs) comme (l+p1)+p2. L'opration l+p1 rendra la rfrence l. L'opration (l+p1)+p2 devient alors l+p2 qui ajoute la personne p2 la liste de personnes l. ligne 16 : nous redfinissons la mthode ToString afin d'afficher une liste de personnes sous la forme (personne1, personne2, ..) o personnei est lui-mme le rsultat de la mthode ToString de la classe Personne. ligne 19 : nous utilisons un objet de type StringBuilder. Cette classe convient mieux que la classe string ds qu'il faut faire de nombreuses oprations sur la chane de caractres, ici des ajouts. En effet, chaque opration sur un objet string rend un nouvel objet string, alors que les mmes oprations sur un objet StringBuilder modifient l'objet mais n'en crent pas un nouveau. Nous utilisons la mthode Append pour concatner les chanes de caractres. ligne 21 : on parcourt les lments de la liste de personnes. Cette liste est ici dsigne par this. C'est l'objet courant sur laquelle est excute la mthode ToString. La proprit Count est une proprit de la classe parent ArrayList. ligne 22 : l'lment n i de la liste courante this est accessible via la notation this[i]. L encore, c'est une proprit de la classe ArrayList. Comme il s'agit d'ajouter des chanes, c'est la mthode this[i].ToString() qui va tre utilise. Comme cette mthode est virtuelle, c'est la mthode ToString de l'objet this, de type Personne ou driv, qui va tre utilise. ligne 31 : il nous faut rendre un objet de type string (ligne 16). La classe StringBuilder a une mthode ToString qui permet de passer d'un type StringBuilder un type string.

On notera que la classe ListeDePersonnes n'a pas de constructeur. Dans ce cas, on sait que le constructeur

Classes, Stuctures, Interfaces

62

public ListeDePersonnes(){ }

sera utilis. Ce constructeur ne fait rien si ce n'est appeler le constructeur sans paramtres de sa classe parent :
public ArrayList(){ ... }

Une classe de test pourrait tre la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using System; namespace Chap2 { class Program1 { static void Main(string[] args) { // une liste de personnes ListeDePersonnes l = new ListeDePersonnes(); // ajout de personnes l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12); // affichage Console.WriteLine("l=" + l); l = l + new Enseignant("camille", "germain",27,60); Console.WriteLine("l=" + l); } } }

ligne 7 : cration d'une liste de personnes l ligne 9 : ajout de 2 personnes avec l'oprateur + ligne 12 : ajout d'un enseignant lignes 11 et 13 : utilisation de la mthode redfinie ListeDePersonnes.ToString().

Les rsultats :
1. 2. l=([jean, martin, 10],[pauline, leduc, 12]) l=([jean, martin, 10],[pauline, leduc, 12],Enseignant[[camille, germain, 27],60])

2.4

Dfinir un indexeur pour une classe

Nous continuons ici utiliser la classe ListeDePersonnes. Si l est un objet ListeDePersonnes, nous souhaitons pouvoir utiliser la notation l[i] pour dsigner la personne n i de la liste l aussi bien en lecture (Personne p=l[i]) qu'en criture (l[i]=new Personne(...)). Pour pouvoir crire l[i] o l[i] dsigne un objet Personne, il nous faut dfinir dans la classe ListeDePersonnes la mthode this suivante :
1. 2. 3. 4. public Personne this[int i] { get { ... } set { ... } }

On appelle la mthode this[int i], un indexeur car elle donne une signification l'expression obj[i] qui rappelle la notation des tableaux alors que obj n'est pas un tableau mais un objet. La mthode get de la mthode this de l'objet obj est appele lorsqu'on crit variable=obj[i] et la mthode set lorsqu'on crit obj[i]=valeur. La classe ListeDePersonnes drive de la classe ArrayList qui a elle-mme un indexeur :
public object this[int i] { ... }

Il y a un conflit entre la mthode this de la classe ListeDePersonnes :


public Personne this[int i]

et la mthode this de la classe ArrayList


public object this[int i]

Classes, Stuctures, Interfaces

63

parce qu'elles portent le mme nom et admettent le mme type de paramtre (int).Pour indiquer que la mthode this de la classe ListeDePersonnes "cache" la mthode de mme nom de la classe ArrayList, on est oblig d'ajouter le mot cl new la dclaration de l'indexeur de ListeDePersonnes. On crira donc :
public new Personne this[int i]{ get { ... } set { ... } }

Compltons cette mthode. La mthode this.get est appele lorsqu'on crit variable=l[i] par exemple, o l est de type ListeDePersonnes. On doit alors retourner la personne n i de la liste l. Ceci se fait avec la notation base[i], qui rend l'objet n i de la classe ArrayList sous-jacente la classe ListeDePersonnes . L'objet retourn tant de type Object, un transtypage vers la classe Personne est ncessaire.
public new Personne this[int i]{ get { return (Personne) base[i]; } set { ... } }

La mthode set est appele lorsqu'on crit l[i]=p o p est une Personne. Il s'agit alors d'affecter la personne p l'lment i de la liste l.
public new Personne this[int i]{ get { ... } set { base[i]=value; } }

Ici, la personne p reprsente par le mot cl value est affecte l'lment n i de la classe de base ArrayList. L'indexeur de la classe ListeDePersonnes sera donc le suivant :
public new Personne this[int i]{ get { return (Personne) base[i]; } set { base[i]=value; } }

Maintenant, on veut pouvoir crire galement Personne p=l["nom"], c.a.d indexer la liste l non plus par un n d'lment mais par un nom de personne. Pour cela on dfinit un nouvel indexeur :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. } // indexeur via un nom public int this[string nom] { get { // on recherche la personne for (int i = 0; i < Count; i++) { if (((Personne)base[i]).Nom == nom) return i; }//for return -1; }//get

La premire ligne
public int this[string nom]

indique qu'on indexe la classe ListeDePersonnes par une chane de caractres nom et que le rsultat de l[nom] est un entier. Cet entier sera la position dans la liste, de la personne portant le nom nom ou -1 si cette personne n'est pas dans la liste. On ne dfinit que la proprit get, interdisant ainsi l'criture l["nom"]=valeur qui aurait ncessit la dfinition de la proprit set. Le mot cl new n'est pas ncessaire dans la dclaration de l'indexeur car la classe de base ArrayList ne dfinit pas d'indexeur this[string]. Dans le corps du get, on parcourt la liste des personnes la recherche du nom pass en paramtre. Si on le trouve en position i, on renvoie i sinon on renvoie -1. Le programme de test prcdent est complt de la faon suivante :
1. 2. 3. 4. 5. 6. using System; namespace Chap2 { class Program2 { static void Main(string[] args) { // une liste de personnes

Classes, Stuctures, Interfaces

64

7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. } 31. }

ListeDePersonnes l = new ListeDePersonnes(); // ajout de personnes l = l + new Personne("jean", "martin",10) + new Personne("pauline", "leduc",12); // affichage Console.WriteLine("l=" + l); l = l + new Enseignant("camille", "germain",27,60); Console.WriteLine("l=" + l); // changement lment 1 l[1] = new Personne("franck", "gallon",5); // affichage lment 1 Console.WriteLine("l[1]=" + l[1]); // affichage liste l Console.WriteLine("l=" + l); // recherche de personnes string[] noms = { "martin", "germain", "xx" }; for (int i = 0; i < noms.Length; i++) { int inom = l[noms[i]]; if (inom != -1) Console.WriteLine("Personne(" + noms[i] + ")=" + l[inom]); else Console.WriteLine("Personne(" + noms[i] + ") n'existe pas"); }//for

Son excution donne les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. l=([jean, martin, 10],[pauline, leduc, l=([jean, martin, 10],[pauline, leduc, l[1]=[franck, gallon, 5] l=([jean, martin, 10],[franck, gallon, Personne(martin)=[jean, martin, 10] Personne(germain)=Enseignant[[camille, Personne(xx) n'existe pas 12]) 12],Enseignant[[camille, germain, 27],60]) 5],Enseignant[[camille, germain, 27],60]) germain, 27],60]

2.5

Les structures

La structure C# est analogue la structure du langage C et est trs proche de la notion de classe. Une structure est dfinie comme suit :
struct NomStructure{ // attributs ... // proprits ... // constructeurs ... // mthodes ... }

Il y a, malgr une similitude de dclaration des diffrences importantes entre classe et structure. La notion d'hritage n'existe par exemple pas avec les structures. Si on crit une classe qui ne doit pas tre drive, quelles sont les diffrences entre structure et classe qui vont nous aider choisir entre les deux ? Aidons-nous de l'exemple suivant pour le dcouvrir :
1. using System; 2. 3. namespace Chap2 { 4. class Program1 { 5. static void Main(string[] args) { 6. // une structure sp1 7. SPersonne sp1; 8. sp1.Nom = "paul"; 9. sp1.Age = 10; 10. Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")"); 11. // une structure sp2 12. SPersonne sp2 = sp1; 13. Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")"); 14. // sp2 est modifi 15. sp2.Nom = "nicole";

Classes, Stuctures, Interfaces

65

16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49.

sp2.Age = 30; // vrification sp1 et sp2 Console.WriteLine("sp1=SPersonne(" + sp1.Nom + "," + sp1.Age + ")"); Console.WriteLine("sp2=SPersonne(" + sp2.Nom + "," + sp2.Age + ")"); // un objet op1 CPersonne op1=new CPersonne(); op1.Nom = "paul"; op1.Age = 10; Console.WriteLine("op1=CPersonne(" // un objet op2 CPersonne op2=op1; Console.WriteLine("op2=CPersonne(" // op2 est modifi op2.Nom = "nicole"; op2.Age = 30; // vrification op1 et op2 Console.WriteLine("op1=CPersonne(" Console.WriteLine("op2=CPersonne(" } } // structure SPersonne struct SPersonne { public string Nom; public int Age; } // classe CPersonne class CPersonne { public string Nom; public int Age; } }

+ op1.Nom + "," + op1.Age + ")"); + op2.Nom + "," + op2.Age + ")");

+ op1.Nom + "," + op1.Age + ")"); + op2.Nom + "," + op2.Age + ")");

lignes 38-41 : une structure avec deux champs publics : Nom, Age lignes 44-47 : une classe avec deux champs publics : Nom, Age

Si on excute ce programme, on obtient les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. sp1=SPersonne(paul,10) sp2=SPersonne(paul,10) sp1=SPersonne(paul,10) sp2=SPersonne(nicole,30) op1=CPersonne(paul,10) op2=CPersonne(paul,10) op1=CPersonne(nicole,30) op2=CPersonne(nicole,30)

L o prcdemment on utilisait une classe Personne, nous utilisons maintenant une structure SPersonne :
1. 2. 3. 4. struct SPersonne { public string Nom; public int Age; }

La structure n'a ici pas de constructeur. Elle pourrait en avoir un comme nous le montrerons plus loin. Par dfaut, elle dispose toujours du constructeur sans paramtres, ici SPersonne().

ligne 7 du code : la dclaration


SPersonne sp1;

est quivalente l'instruction :


SPersonne sp1=new Spersonne();

Une structure (Nom,Age) est cre et la valeur de sp1 est cette structure elle-mme. Dans le cas de la classe, la cration de l'objet (Nom,Age) doit se faire explicitement par l'oprateur new (ligne 22) :
CPersonne op1=new CPersonne();

Classes, Stuctures, Interfaces

66

L'instruction prcdente cre un objet CPersonne (grosso modo l'quivalent de notre structure) et la valeur de p1 est alors l'adresse (la rfrence) de cet objet. Rsumons dans le cas de la structure, la valeur de sp1 est la structure elle-mme dans le cas de la classe, la valeur de op1 est l'adresse de l'objet cr Structure p1 Nom Age sp1 Objet p1 Nom Age

op1

Lorsque dans le programme on crit ligne 12 :


SPersonne sp2 = sp1;

une nouvelle structure sp2(Nom,Age) est cre et initialise avec la valeur de sp1, donc la structure elle-mme. sp1 paul 10 1 sp2 paul 10 op1 op2 ---------> ---------> paul 10 2

La structure de sp1 est duplique dans sp2 [1]. C'est une recopie de valeur. Considrons maintenant l'instruction, ligne 27 :
CPersonne op2=op1;

Dans le cas des classes, la valeur de op1 est recopie dans op2, mais comme cette valeur est en fait l'adresse de l'objet, celui-ci n'est pas dupliqu [2]. Dans le cas de la structure [1], si on modifie la valeur de sp2 on ne modifie pas la valeur de sp1, ce que montre le programme. Dans le cas de l'objet [2], si on modifie l'objet point par op2, celui point par op1 est modifi puisque c'est le mme. C'est ce que montrent galement les rsultats du programme. On retiendra donc de ces explications que : la valeur d'une variable de type structure est la structure elle-mme la valeur d'une variable de type objet est l'adresse de l'objet point

Une fois cette diffrence fondamentale comprise, la structure se montre trs proche de la classe comme le montre le nouvel exemple suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. using System; namespace Chap2 { // structure SPersonne struct SPersonne { // attributs privs private string nom; private int age; // proprits public string Nom { get { return nom; }

Classes, Stuctures, Interfaces

67

14. set { nom = value; } 15. }//nom 16. 17. public int Age { 18. get { return age; } 19. set { age = value; } 20. }//age 21. 22. // Constructeur 23. public SPersonne(string nom, int age) { 24. this.nom = nom; 25. this.age = age; 26. }//constructeur 27. 28. // ToString 29. public override string ToString() { 30. return "SPersonne(" + Nom + "," + Age + ")"; 31. }//ToString 32. }//structure 33. }//namespace

lignes 8-9 : deux champs privs lignes 12-20 : les proprits publiques associes lignes 23-26 : on dfinit un constructeur. A noter que le constructeur sans paramtres SPersonne() est toujours prsent et n'a pas tre dclar. Sa dclaration est refuse par le compilateur. Dans le constructeur des lignes 23-26, on pourrait tre tent d'initialiser les champs privs nom, age via leurs proprits publiques Nom, Age. C'est refus par le compilateur. Les mthodes de la structure ne peuvent tre utilises lors de la construction de celle-ci. lignes 29-31 : redfinition de la mthode ToString.

Un programme de test pourrait tre le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; namespace Chap2 { class Program1 { static void Main(string[] args) { // une personne p1 SPersonne p1=new SPersonne(); p1.Nom="paul"; p1.Age= 10; Console.WriteLine("p1={0}",p1); // une personne p2 SPersonne p2 = p1; Console.WriteLine("p2=" + p2); // p2 est modifi p2.Nom = "nicole"; p2.Age = 30; // vrification p1 et p2 Console.WriteLine("p1=" + p1); Console.WriteLine("p2=" + p2); // une personne p3 SPersonne p3 = new SPersonne("amandin", 18); Console.WriteLine("p3=" + p3); // une personne p4 SPersonne p4 = new SPersonne { Nom = "x", Age = 10 }; Console.WriteLine("p4=" + p4); } } }

ligne 7 : on est obligs d'utiliser explicitement le constructeur sans paramtres, ceci parce qu'il existe un autre constructeur dans la structure. Si la structure n'avait eu aucun constructeur, l'instruction
SPersonne p1;

aurait suffi pour crer une structure vide. lignes 8-9 : la structure est initialise via ses proprits publiques ligne 10 : la mthode p1.ToString va tre utilise dans le WriteLine. ligne 21 : cration d'une structure avec le constructeur SPersonne(string,int) ligne 24 : cration d'une structure avec le constructeur sans paramtres SPersonne() avec, entre accolades, initialisation des champs privs via leurs proprits publiques.

Classes, Stuctures, Interfaces

68

On obtient les rsultats d'excution suivants :


1. 2. 3. 4. 5. 6. p1=SPersonne(paul,10) p2=SPersonne(paul,10) p1=SPersonne(paul,10) p2=SPersonne(nicole,30) p3=SPersonne(amandin,18) p4=SPersonne(x,10)

La seule diffrence notable ici entre structure et classe, c'est qu'avec une classe les objets p1 et p2 auraient point sur le mme objet la fin du programme.

2.6

Les interfaces

Une interface est un ensemble de prototypes de mthodes ou de proprits qui forme un contrat. Une classe qui dcide d'implmenter une interface s'engage fournir une implmentation de toutes les mthodes dfinies dans l'interface. C'est le compilateur qui vrifie cette implmentation. Voici par exemple la dfinition de l'interface System.Collections.IEnumerator :
public interface System.Collections.IEnumerator { // Properties Object Current { get; } // Methods bool MoveNext(); void Reset(); }

Les proprits et mthodes de l'interface ne sont dfinies que par leurs signatures. Elles ne sont pas implmentes (n'ont pas de code). Ce sont les classes qui implmentent l'interface qui donnent du code aux mthodes et proprits de l'interface.
1. 2. 3. 4. 5. 6. public class C : IEnumerator{ ... Object Current{ get {...}} bool MoveNext{...} void Reset(){...} }

ligne 1 : la classe C implmente la classe IEnumerator. On notera que le signe : utilis pour l'implmentation d'une interface est le mme que celui utilis pour la drivation d'une classe. lignes 3-5 : l'implmentation des mthodes et proprits de l'interface IEnumerator.

Considrons l'interface suivante :


1. 2. 3. 4. 5. 6. namespace Chap2 { public interface IStats { double Moyenne { get; } double EcartType(); } }

L'interface IStats prsente : une proprit en lecture seule Moyenne : pour calculer la moyenne d'une srie de valeurs une mthode EcartType : pour en calculer l'cart-type On notera qu'il n'est nulle part prcis de quelle srie de valeurs il s'agit. Il peut s'agir de la moyenne des notes d'une classe, de la moyenne mensuelle des ventes d'un produit particulier, de la temprature moyenne dans un lieu donn, ... C'est le principe des interfaces : on suppose l'existence de mthodes dans l'objet mais pas celle de donnes particulires. Une premire classe d'implmentation de l'interface IStats pourrait une classe servant mmoriser les notes des lves d'une classe dans une matire donne. Un lve serait caractris par la structure Elve suivante :
1. public struct Elve {

Classes, Stuctures, Interfaces

69

2. 3. 4.

public string Nom { get; set; } public string Prnom { get; set; } }//Elve

L'lve serait identifi par son nom et son prnom. Lignes 2-3, on trouve les proprits automatiques pour ces deux attributs. Une note serait caractrise par la structure Note suivante :
1. 2. 3. 4. public struct Note { public Elve Elve { get; set; } public double Valeur { get; set; } }//Note

La note serait identifie par l'lve not et la note elle-mme. Lignes 2-3, on trouve les proprits automatiques pour ces deux attributs. Les notes de tous les lves dans une matire donne sont rassembles dans la classe TableauDeNotes suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. using System; using System.Text; namespace Chap2 { public class TableauDeNotes : IStats { // attributs public string Matire { get; set; } public Note[] Notes { get; set; } public double Moyenne { get; private set; } private double ecartType; // constructeur public TableauDeNotes(string matire, Note[] notes) { // mmorisation via les proprits publiques Matire = matire; Notes = notes; // calcul de la moyenne des notes double somme = 0; for (int i = 0; i < Notes.Length; i++) { somme += Notes[i].Valeur; } if (Notes.Length != 0) Moyenne = somme / Notes.Length; else Moyenne = -1; // cart-type double carrs = 0; for (int i = 0; i < Notes.Length; i++) { carrs += Math.Pow((Notes[i].Valeur - Moyenne), 2); }//for if (Notes.Length != 0) ecartType = Math.Sqrt(carrs / Notes.Length); else ecartType = -1; }//constructeur public double EcartType() { return ecartType; } // ToString public override string ToString() { StringBuilder valeur = new StringBuilder(String.Format("matire={0}, notes=(", Matire)); int i; // on concatne toutes les notes for (i = 0; i < Notes.Length-1; i++) { valeur.Append("[").Append(Notes[i].Elve.Prnom).Append(",").Append(Notes[i].Elve.Nom).Append("," ).Append(Notes[i].Valeur).Append("],"); }; //dernire note if (Notes.Length != 0) { valeur.Append("[").Append(Notes[i].Elve.Prnom).Append(",").Append(Notes[i].Elve.Nom).Append("," ).Append(Notes[i].Valeur).Append("]"); } valeur.Append(")"); // fin return valeur.ToString(); }//ToString

Classes, Stuctures, Interfaces

70

55. 56. }//classe 57. }

ligne 6 : la classe TableauDeNotes implmente l'interface IStats. Elle doit donc implmenter la proprit Moyenne et la mthode EcartType. Celles-ci sont implmentes lignes 10 (Moyenne) et 35-37 (EcartType) lignes 8-10 : trois proprits automatiques ligne 8 : la matire dont l'objet mmorise les notes ligne 9 : le tableau des notes des lves (Elve, Note) ligne 10 : la moyenne des notes - proprit implmentant la proprit Moyenne de l'interface IStats. ligne 11 : champ mmorisant l'cart-type des notes - la mthode get associe EcartType des lignes 35-37 implmente la mthode EcartType de l'interface IStats. ligne 9 : les notes sont mmorises dans un tableau. Celui-ci est transmis lors de la construction de la classe TableauDeNotes au constructeur des lignes 14-33. lignes 14-33 : le constructeur. On suppose ici que les notes transmises au constructeur ne bougeront plus par la suite. Aussi utilise-t-on le constructeur pour calculer tout de suite la moyenne et l'cart-type de ces notes et les mmoriser dans les champs des lignes 10-11. La moyenne est mmorise dans le champ priv sous-jacent la proprit automatique Moyenne de la ligne 10 et l'cart-type dans le champ priv de la ligne 11. ligne 10 : la mthode get de la proprit automatique Moyenne rendra le champ priv sous-jacent. lignes 35-37 : la mthode EcartType rend la valeur du champ priv de la ligne 11.

Il y a quelques subtilits dans ce code :


ligne 23 : la mthode set de la proprit Moyenne est utilise pour faire l'affectation. Cette mthode a t dclare prive ligne 10 afin que l'affectation d'une valeur la proprit Moyenne ne soit possible qu' l'intrieur de la classe. lignes 40-54 : utilisent un objet StringBuilder pour construire la chane reprsentant l'objet TableauDeNotes afin d'amliorer les performances. On peut noter que la lisibilit du code en ptit beaucoup. C'est le revers de la mdaille.

Dans la classe prcdente, les notes taient enregistres dans un tableau. Il n'tait pas possible d'ajouter une nouvelle note aprs construction de l'objet TableauDeNotes. Nous proposons maintenant une seconde implmentation de l'interface IStats, appele ListeDeNotes, o cette fois les notes seraient enregistres dans une liste, avec possibilit d'ajouter des notes aprs construction initiale de l'objet ListeDeNotes. Le code de la classe ListeDeNotes est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. using System; using System.Text; using System.Collections.Generic; namespace Chap2 { public class ListeDeNotes : IStats { // attributs public string Matire { get; set; } public List<Note> Notes { get; set; } public double moyenne = -1; public double ecartType = -1; // constructeur public ListeDeNotes(string matire, List<Note> notes) { // mmorisation via les proprits publiques Matire = matire; Notes = notes; }//constructeur // ajout d'une note public void Ajouter(Note note) { // ajout de la note Notes.Add(note); // moyenne et cart type rinitialiss moyenne = -1; ecartType = -1; } // ToString public override string ToString() { StringBuilder valeur = new StringBuilder(String.Format("matire={0}, notes=(", Matire)); int i; // on concatne toutes les notes

Classes, Stuctures, Interfaces

71

35. for (i = 0; i < Notes.Count - 1; i++) { 36. valeur.Append("[").Append(Notes[i].Elve.Prnom).Append(",").Append(Notes[i].Elve.Nom).Append("," ).Append(Notes[i].Valeur).Append("],"); 37. }; 38. //dernire note 39. if (Notes.Count != 0) { 40. valeur.Append("[").Append(Notes[i].Elve.Prnom).Append(",").Append(Notes[i].Elve.Nom).Append("," ).Append(Notes[i].Valeur).Append("]"); 41. } 42. valeur.Append(")"); 43. // fin 44. return valeur.ToString(); 45. }//ToString 46. 47. // moyenne des notes 48. public double Moyenne { 49. get { 50. if (moyenne != -1) return moyenne; 51. // calcul de la moyenne des notes 52. double somme = 0; 53. for (int i = 0; i < Notes.Count; i++) { 54. somme += Notes[i].Valeur; 55. } 56. // on rend la moyenne 57. if (Notes.Count != 0) moyenne = somme / Notes.Count; 58. return moyenne; 59. } 60. } 61. 62. public double EcartType() { 63. // cart-type 64. if (ecartType != -1) return ecartType; 65. // moyenne 66. double moyenne = Moyenne; 67. double carrs = 0; 68. for (int i = 0; i < Notes.Count; i++) { 69. carrs += Math.Pow((Notes[i].Valeur - moyenne), 2); 70. }//for 71. // on rend l'cart type 72. if (Notes.Count != 0) 73. ecartType = Math.Sqrt(carrs / Notes.Count); 74. return ecartType; 75. } 76. }//classe 77. }

ligne 7 : la classe ListeDeNotes implmente l'interface IStats ligne 10 : les notes sont mises maintenant dans une liste plutt qu'un tableau ligne 11 : la proprit automatique Moyenne de la classe TableauDeNotes a t abandonne ici au profit d'un champ priv moyenne, ligne 11, associ la proprit publique en lecture seule Moyenne des lignes 48-60 lignes 22-28 : on peut dsormais ajouter une note celles dj mmorises, ce qu'on ne pouvait pas faire prcdemment. lignes 15-19 : du coup, la moyenne et l'cart-type ne sont plus calculs dans le constructeur mais dans les mthodes de l'interface elles-mmes : Moyenne (lignes 48-60) et EcartType (62-76). Le recalcul n'est cependant relanc que si la moyenne et l'cart-type sont diffrents de -1 (lignes 50 et 64).

Une classe de test pourrait tre la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. using System; using System.Collections.Generic; namespace Chap2 { class Program1 { static void Main(string[] args) { // qqs lves & notes d'anglais Elve[] lves1 = { new Elve { Prnom = "Paul", Nom = "Martin" }, new Elve { Prnom = "Maxime", Nom = "Germain" }, new Elve { Prnom = "Berthine", Nom = "Samin" } }; Note[] notes1 = { new Note { Elve = lves1[0], Valeur = 14 }, new Note { Elve = lves1[1], Valeur = 16 }, new Note { Elve = lves1[2], Valeur = 18 } }; // qu'on enregistre dans un objet TableauDeNotes TableauDeNotes anglais = new TableauDeNotes("anglais", notes1); // affichage moyenne et cart-type Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", anglais.Moyenne, anglais.EcartType(), anglais); // on met les lves et la matire dans un objet ListeDeNotes

Classes, Stuctures, Interfaces

72

15. 16. 17. 18. 19. 20. 21. 22. 23. 24.

ListeDeNotes franais = new ListeDeNotes("franais", new List<Note>(notes1)); // affichage moyenne et cart-type Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", franais.Moyenne, franais.EcartType(), franais); // on rajoute une note franais.Ajouter(new Note { Elve = new Elve { Prnom = "Jrme", Nom = "Jaric" }, Valeur = 10 }); // affichage moyenne et cart-type Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", franais.Moyenne, franais.EcartType(), franais); } } }

ligne 8 : cration d'un tableau d'lves avec utilisation du constructeur sans paramtres et initialisation via les proprits publiques ligne 9 : cration d'un tableau de notes selon la mme technique ligne 11 : un objet TableauDeNotes dont on calcule la moyenne et l'cart-type ligne 13 ligne 15 : un objet ListeDeNotes dont on calcule la moyenne et l'cart-type ligne 17. La classe List<Note> a un constructeur admettant un objet implmentant l'interface IEnumerable<Note>. Le tableau notes1 implmente cette interface et peut tre utilis pour construire l'objet List<Note>. ligne 19 : ajout d'une nouvelle note ligne 21 : recalcul de la moyenne et cart-type

Les rsultats de l'excution sont les suivants :


1. 2. 3. matire=anglais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545 matire=franais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18]), Moyenne=16, Ecart-type=1,63299316185545 matire=franais, notes=([Paul,Martin,14],[Maxime,Germain,16],[Berthine,Samin,18], [Jrme,Jaric,10]), Moyenne=14,5, Ecart-type=2,95803989154981

Dans l'exemple prcdent, deux classes implmentent l'interface IStats. Ceci dit, l'exemple ne fait pas apparatre l'intrt de l'interface IStats. Rcrivons le programme de test de la faon suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. using System; using System.Collections.Generic; namespace Chap2 { class Program2 { static void Main(string[] args) { // qqs lves & notes d'anglais Elve[] lves1 = { new Elve { Prnom = "Paul", Nom = "Martin" }, new Elve { Prnom = "Maxime", Nom = "Germain" }, new Elve { Prnom = "Berthine", Nom = "Samin" } }; Note[] notes1 = { new Note { Elve = lves1[0], Valeur = 14 }, new Note { Elve = lves1[1], Valeur = 16 }, new Note { Elve = lves1[2], Valeur = 18 } }; // qu'on enregistre dans un objet TableauDeNotes TableauDeNotes anglais = new TableauDeNotes("anglais", notes1); // affichage moyenne et cart-type AfficheStats(anglais); // on met les lves et la matire dans un objet ListeDeNotes ListeDeNotes franais = new ListeDeNotes("franais", new List<Note>(notes1)); // affichage moyenne et cart-type AfficheStats(franais); // on rajoute une note franais.Ajouter(new Note { Elve = new Elve { Prnom = "Jrme", Nom = "Jaric" }, Valeur = 10 }); // affichage moyenne et cart-type AfficheStats(franais); }

// affichage moyenne et cart-type d'un type IStats static void AfficheStats(IStats valeurs) { Console.WriteLine("{2}, Moyenne={0}, Ecart-type={1}", valeurs.Moyenne, valeurs.EcartType(), valeurs); 27. } 28. } 29. }

lignes 25-27 : la mthode statique AfficheStats reoit pour paramtre un type IStats, donc un type Interface. Cela signifie que le paramtre effectif peut tre tout objet implmentant l'interface IStats. Quand on utilise une donne ayant le

Classes, Stuctures, Interfaces

73

type d'une interface, cela signifie qu'on n'utilisera que les mthodes de l'interface implmentes par la donne. On fait abstraction du reste. On a l une proprit proche du polymorphisme vu pour les classes. Si un ensemble de classes Ci non lies entre-elles par hritage (donc on ne peut utiliser le polymorphisme de l'hritage) prsente un ensemble de mthodes de mme signature, il peut tre intressant de regrouper ces mthodes dans une interface I qu'implmenteraient toutes les classes concernes. Des instances de ces classes Ci peuvent alors tre utilises comme paramtres effectifs de fonctions admettant un paramtre formel de type I, c.a.d. des fonctions n'utilisant que les mthodes des objets Ci dfinies dans l'interface I et non les attributs et mthodes particuliers des diffrentes classes Ci. ligne 13 : la mthode AfficheStats est appele avec un type TableauDeNotes qui implmente l'interface IStats ligne 17 : idem avec un type ListeDeNotes

Les rsultats de l'excution sont identiques ceux de la prcdente. Une variable peut tre du type d'une interface. Ainsi, on peut crire :
1. 2. 3. IStats stats1=new TableauDeNotes(...); ... stats1=new ListeDeNotes(...);

La dclaration de la ligne 1 indique que stats1 est l'instance d'une classe implmentant l'interface IStats. Cette dclaration implique que le compilateur ne permettra l'accs dans stats1 qu'aux mthodes de l'interface : la proprit Moyenne et la mthode EcartType. Notons enfin que l'implmentation d'interfaces peut tre multiple, c.a.d. qu'on peut crire
public class ClasseDrive:ClasseDeBase,I1,I2,..,In{ ... }

o les Ij sont des interfaces.

2.7

Les classes abstraites

Une classe abstraite est une classe qu'on ne peut instancier. Il faut crer des classes drives qui elles pourront tre instancies. On peut utiliser des classes abstraites pour factoriser le code d'une ligne de classes. Examinons le cas suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. using System; namespace Chap2 { abstract class Utilisateur { // champs private string login; private string motDePasse; private string role; // constructeur public Utilisateur(string login, string motDePasse) { // on enregistre les informations this.login = login; this.motDePasse = motDePasse; // on identifie l'utilisateur role=identifie(); // identifi ? if (role == null) { throw new ExceptionUtilisateurInconnu(String.Format("[{0},{1}]", login, motDePasse)); } } // toString public override string ToString() { return String.Format("Utilisateur[{0},{1},{2}]", login, motDePasse, role); } // identifie abstract public string identifie();

} }

Classes, Stuctures, Interfaces

74

lignes 11-21 : le contructeur de la classe Utilisateur. Cette classe mmorise des informations sur l'utilisateur d'une application web. Celle-ci a divers types d'utilisateurs authentifis par un login / mot de passe (lignes 6-7). Ces deux informations sont vrifies auprs d'un service LDAP pour certains utilisateurs, auprs d'un SGBD pour d'autres, etc... lignes 13-14 : les informations d'authentification sont mmorises ligne 16 : elles sont vrifies par une mthode identifie. Parce que la mthode d'identification n'est pas connue, elle est dclare abstraite ligne 29 avec le mot cl abstract. La mthode identifie rend une chane de caractres prcisant le rle de l'utilisateur (en gros ce qu'il a le droit de faire). Si cette chane est le pointeur null, une exception est lance ligne 19. ligne 4 : parce qu'elle a une mthode abstraite, la classe elle-mme est dclare abstraite avec le mot cl abstract. ligne 29 : la mthode abstraite identifie n'a pas de dfinition. Ce sont les classes drives qui lui en donneront une. lignes 24-26 : la mthode ToString qui identifie une instance de la classe.

On suppose ici que le dveloppeur veut avoir la matrise de la construction des instances de la classe Utilisateur et des classes drives, peut-tre parce qu'il veut tre sr qu'une exception d'un certain type est lance si l'utilisateur n'est pas reconnu (ligne 19). Les classes drives pourront s'appuyer sur ce constructeur. Elles devront pour cela fournir la mthode identifie. La classe ExceptionUtilisateurInconnu est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. using System; namespace Chap2 { class ExceptionUtilisateurInconnu : Exception { public ExceptionUtilisateurInconnu(string message) : base(message){ } } }

ligne 3 : elle drive de la classe Exception lignes 4-6 : elle n'a qu'un unique constructeur qui admet pour paramtre un message d'erreur. Celui-ci est pass la classe parent (ligne 5) qui a ce mme constructeur.

Nous drivons maintenant la classe Utilisateur dans la classe fille Administrateur :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. namespace Chap2 { class Administrateur : Utilisateur { // constructeur public Administrateur(string login, string motDePasse) : base(login, motDePasse) { } // identifie public override string identifie() { // identification LDAP // ... return "admin"; } } }

lignes 4-6 : le constructeur se contente de passer sa classe parent les paramtres qu'il reoit lignes 9-12 : la mthode identifie de la classe Administrateur. On suppose qu'un administrateur est identifi par un systme LDAP. Cette mthode redfinit la mthode identifie de sa classe parent. Parce qu'elle redfinit une mthode abstraite, il est inutile de mettre le mot cl override.

Nous drivons maintenant la classe Utilisateur dans la classe fille Observateur :


1. namespace Chap2 { 2. class Observateur : Utilisateur{ 3. // constructeur 4. public Observateur(string login, string motDePasse) 5. : base(login, motDePasse) { 6. } 7. 8. //identifie 9. public override string identifie() { 10. // identification SGBD 11. // ... 12. return "observateur"; 13. } 14.

Classes, Stuctures, Interfaces

75

15. } 16. }

lignes 4-6 : le constructeur se contente de passer sa classe parent les paramtres qu'il reoit lignes 9-13 : la mthode identifie de la classe Observateur. On suppose qu'un observateur est identifi par vrification de ses donnes d'identification dans une base de donnes.

Au final, les objets Administrateur et Observateur sont instancis par le mme constructeur, celui de la classe parent Utilisateur. Ce constructeur va utiliser la mthode identifie que ces classes fournissent. Une troisime classe Inconnu drive galement de la classe Utilisateur :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. namespace Chap2 { class Inconnu : Utilisateur{ // constructeur public Inconnu(string login, string motDePasse) : base(login, motDePasse) { } //identifie public override string identifie() { // utilisateur pas connu // ... return null; } } }

ligne 13 : la mthode identifie rend le pointeur null pour indiquer que l'utilisateur n'a pas t reconnu.

Un programme de test pourrait tre le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. using System; namespace Chap2 { class Program { static void Main(string[] args) { Console.WriteLine(new Observateur("observer","mdp1")); Console.WriteLine(new Administrateur("admin", "mdp2")); try { Console.WriteLine(new Inconnu("xx", "yy")); } catch (ExceptionUtilisateurInconnu e) { Console.WriteLine("Utilisateur non connu : "+ e.Message); } } } }

On notera que lignes 6, 7 et 9, c'est la mthode [Utilisateur].ToString() qui sera utilise par la mthode WriteLine. Les rsultats de l'excution sont les suivants :
1. 2. 3. Utilisateur[observer,mdp1,observateur] Utilisateur[admin,mdp2,admin] Utilisateur non connu : [xx,yy]

2.8

Les classes, interfaces, mthodes gnriques

Supposons qu'on veuille crire une mthode permutant deux nombres entiers. Cette mthode pourrait tre la suivante :
1. 2. 3. 4. 5. 6. public static void Echanger1(ref int value1, ref int value2){ // on change les rfrences value1 et value2 int temp = value2; value2 = value1; value1 = temp; }

Classes, Stuctures, Interfaces

76

Maintenant, si on voulait permuter deux rfrences sur des objets Personne, on crirait :
1. 2. 3. 4. 5. 6. public static void Echanger2(ref Personne value1, ref Personne value2){ // on change les rfrences value1 et value2 Personne temp = value2; value2 = value1; value1 = temp;

Ce qui diffrencie les deux mthodes, c'est le type T des paramtres : int dans Echanger1, Personne dans Echanger2. Les classes et interfaces gnriques rpondent au besoin de mthodes qui ne diffrent que par le type de certains de leurs paramtres. Avec une classe gnrique, la mthode Echanger pourrait tre rcrite de la faon suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. namespace Chap2 { class Generic1<T> { public static void Echanger(ref T value1, ref T value2){ // on change les rfrences value1 et value2 T temp = value2; value2 = value1; value1 = temp; } } }

ligne 2 : la classe Generic1 est paramtre par un type not T. On peut lui donner le nom que l'on veut. Ce type T est ensuite rutilis dans la classe aux lignes 3 et 5. On dit que la classe Generic1 est une classe gnrique. ligne 3 : dfinit les deux rfrences sur un type T permuter ligne 5 : la variable temporaire temp a le type T.

Un programme de test de la classe pourrait tre le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. using System; namespace Chap2 { class Program { static void Main(string[] args) { // int int i1 = 1, i2 = 2; Generic1<int>.Echanger(ref i1, ref i2); Console.WriteLine("i1={0},i2={1}", i1, i2); // string string s1 = "s1", s2 = "s2"; Generic1<string>.Echanger(ref s1, ref s2); Console.WriteLine("s1={0},s2={1}", s1, s2); // Personne Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55); Generic1<Personne>.Echanger(ref p1, ref p2); Console.WriteLine("p1={0},p2={1}", p1, p2); } } }

ligne 8 : lorsqu'on utilise une classe gnrique paramtre par des types T1, T2, ... ces derniers doivent tre "instancis". Ligne 8, on utilise la mthode statique Echanger du type Generic1<int> pour indiquer que les rfrences passes la mthode Echanger sont de type int. ligne 12 : on utilise la mthode statique Echanger du type Generic1<string> pour indiquer que les rfrences passes la mthode Echanger sont de type string. ligne 16 : on utilise la mthode statique Echanger du type Generic1<Personne> pour indiquer que les rfrences passes la mthode Echanger sont de type Personne.

Les rsultats de l'excution sont les suivants :


1. 2. 3. i1=2,i2=1 s1=s2,s2=s1 p1=[pauline, dard, 55],p2=[jean, clu, 20]

La mthode Echanger aurait pu galement tre crite de la faon suivante :

Classes, Stuctures, Interfaces

77

1. 2. 3. 4. 5. 6. 7. 8. 9. 10.

namespace Chap2 { class Generic2 { public static void Echanger<T>(ref T value1, ref T value2){ // on change les rfrences value1 et value2 T temp = value2; value2 = value1; value1 = temp; } } }

ligne 2 : la classe Generic2 n'est plus gnrique ligne 3 : la mthode statique Echanger est gnrique

Le programme de test devient alors le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. using System; namespace Chap2 { class Program2 { static void Main(string[] args) { // int int i1 = 1, i2 = 2; Generic2.Echanger<int>(ref i1, ref i2); Console.WriteLine("i1={0},i2={1}", i1, i2); // string string s1 = "s1", s2 = "s2"; Generic2.Echanger<string>(ref s1, ref s2); Console.WriteLine("s1={0},s2={1}", s1, s2); // Personne Personne p1 = new Personne("jean", "clu", 20), p2 = new Personne("pauline", "dard", 55); Generic2.Echanger<Personne>(ref p1, ref p2); Console.WriteLine("p1={0},p2={1}", p1, p2); } } }

lignes 8, 12 et 16 : on appelle la mthode Echanger en prcisant dans <> le type des paramtres. En fait, le compilateur est capable de dduire d'aprs le type des paramtres effectifs, la variante de la mthode Echanger utiliser. Aussi, l'criture suivante est-elle lgale :
Generic2.Echanger(ref i1, ref i2); ... ... Generic2.Echanger(ref s1, ref s2); Generic2.Echanger(ref p1, ref p2);

1. 2. 3. 4. 5.

Lignes 1, 3 et 5 : la variante de la mthode Echanger appele n'est plus prcise. Le compilateur est capable de la dduire de la nature des paramtres effectifs utiliss. On peut mettre des contraintes sur les paramtres gnriques :

Considrons la nouvelle mthode gnrique Echanger suivante :


1. 2. 3. namespace Chap2 { class Generic3 { public static void Echanger<T>(ref T value1, ref T value2) where T : class {

Classes, Stuctures, Interfaces

78

4. 5. 6. 7. 8. 9. } 10. }

// on change les rfrences value1 et value2 T temp = value2; value2 = value1; value1 = temp;

ligne 3 : on exige que le type T soit une rfrence (classe, interface)

Considrons le programme de test suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. using System; namespace Chap2 { class Program4 { static void Main(string[] args) { // int int i1 = 1, i2 = 2; Generic3.Echanger<int>(ref i1, ref Console.WriteLine("i1={0},i2={1}", // string string s1 = "s1", s2 = "s2"; Generic3.Echanger(ref s1, ref s2); Console.WriteLine("s1={0},s2={1}", // Personne Personne p1 = new Personne("jean", Generic3.Echanger(ref p1, ref p2); Console.WriteLine("p1={0},p2={1}", } } }

i2); i1, i2);

s1, s2); "clu", 20), p2 = new Personne("pauline", "dard", 55); p1, p2);

Le compilateur dclare une erreur sur la ligne 8 car le type int n'est pas une classe ou une interface, c'est une structure :

Considrons la nouvelle mthode gnrique Echanger suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. namespace Chap2 { class Generic4 { public static void Echanger<T>(ref T element1, ref T element2) where T : Interface1 { // on rcupre la valeur des 2 lments int value1 = element1.Value(); int value2 = element2.Value(); // si 1er lment > 2ime lment, on change les lments if (value1 > value2) { T temp = element2; element2 = element1; element1 = temp; } } } }

ligne 3 : le type T doit implmenter l'interface Interface1. Celle-ci a une mthode Value, utilise lignes 5 et 6, qui donne la valeur de l'objet de type T. lignes 8-12 : les deux rfrences element1 et element2 ne sont changes que si la valeur de element1 est suprieure la valeur de element2.

L'interface Interface1 est la suivante :


1. 2. 3. 4. 5. namespace Chap2 { interface Interface1 { int Value(); } }

Classes, Stuctures, Interfaces

79

Elle est implmente par la classe Class1 suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. using System; using System.Threading; namespace Chap2 { class Class1 : Interface1 { // valeur de l'objet private int value; // constructeur public Class1() { // attente 1 ms Thread.Sleep(1); // valeur alatoire entre 0 et 99 value = new Random(DateTime.Now.Millisecond).Next(100); } // accesseur champ priv value public int Value() { return value; } // tat de l'instance public override string ToString() { return value.ToString(); }

} }

ligne 5 : Class1 implmente l'interface Interface1 ligne 7 : la valeur d'une instance de Class1 lignes 10-14 : le champ value est initialis avec une valeur alatoire entre 0 et 99 lignes 18-20 : la mthode Value de l'interface Interface1 lignes 23-25 : la mthode ToString de la classe

L'interface Interface1 est galement implmente par la classe Class2 :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. using System; namespace Chap2 { class Class2 : Interface1 { // valeurs de l'objet private int value; private String s; // constructeur public Class2(String s) { this.s = s; value = s.Length; } // accesseur champ priv value public int Value() { return value; } // tat de l'instance public override string ToString() { return s; }

} }

ligne 4 : Class2 implmente l'interface Interface1 ligne 6 : la valeur d'une instance de Class2 lignes 10-13 : le champ value est initialis avec la longueur de la chane de caractres passe au constructeur lignes 16-18 : la mthode Value de l'interface Interface1 lignes 21-22 : la mthode ToString de la classe

Un programme de test pourrait tre le suivant :

Classes, Stuctures, Interfaces

80

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24.

using System; namespace Chap2 { class Program5 { static void Main(string[] args) { // changer des instances de type Class1 Class1 c1, c2; for (int i = 0; i < 5; i++) { c1 = new Class1(); c2 = new Class1(); Console.WriteLine("Avant change --> c1={0},c2={1}", c1, c2); Generic4.Echanger(ref c1, ref c2); Console.WriteLine("Aprs change --> c1={0},c2={1}", c1, c2); } // changer des instances de type Class2 Class2 c3, c4; c3 = new Class2("xxxxxxxxxxxxxx"); c4 = new Class2("xx"); Console.WriteLine("Avant change --> c3={0},c4={1}", c3, c4); Generic4.Echanger(ref c3, ref c4); Console.WriteLine("Avant change --> c3={0},c4={1}", c3, c4); } } }

lignes 8-14 : des instances de type Class1 sont changes lignes 16-22 : des instances de type Class2 sont changes

Les rsultats de l'excution sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. Avant Aprs Avant Aprs Avant Aprs Avant Aprs Avant Aprs Avant Aprs change change change change change change change change change change change change --> --> --> --> --> --> --> --> --> --> --> --> c1=43,c2=79 c1=43,c2=79 c1=72,c2=56 c1=56,c2=72 c1=92,c2=75 c1=75,c2=92 c1=11,c2=47 c1=11,c2=47 c1=31,c2=67 c1=31,c2=67 c3=xxxxxxxxxxxxxx,c4=xx c3=xx,c4=xxxxxxxxxxxxxx

Pour illustrer la notion d'interface gnrique, nous allons trier un tableau de personnes d'abord sur leurs noms, puis sur leurs ages. La mthode qui nous permet de trier un tableau est la mthode statique Sort de la classe Array :

On rappelle qu'une mthode statique s'utilise en prfixant la mthode par le nom de la classe et non par celui d'une instance de la classe. La mthode Sort a diffrentes signatures (elle est surcharge). Nous utiliserons la signature suivante :
public static void Sort<T>(T[] tableau, IComparer<T> comparateur)

Sort une mthode gnrique o T dsigne un type quelconque. La mthode reoit deux paramtres :

T[] tableau : le tableau d'lments de type T trier IComparer<T> comparateur : une rfrence d'objet implmentant l'interface IComparer<T>.

IComparer<T> est une interface gnrique dfinie comme suit :

Classes, Stuctures, Interfaces

81

1. 2. 3.

public interface IComparer<T>{ int Compare(T t1, T t2); }

L'interface IComparer<T> n'a qu'une unique mthode. La mthode Compare : reoit en paramtres deux lments t1 et t2 de type T rend 1 si t1>t2, 0 si t1==t2, -1 si t1<t2. C'est au dveloppeur de donner une signification aux oprateurs <, ==, >. Par exemple, si p1 et p2 sont deux objets Personne, on pourra dire que p1>p2 si le nom de p1 prcde le nom de p2 dans l'ordre alphabtique. On aura alors un tri croissant selon le nom des personnes. Si on veut un tri selon l'ge, on dira que p1>p2 si l'ge de p1 est suprieur l'ge de p2. pour avoir un tri dans l'ordre dcroissant, il suffit d'inverser les rsultats +1 et -1 Nous en savons assez pour trier un tabeau de personnes. Le programme est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. using System; using System.Collections.Generic; namespace Chap2 { class Program6 { static void Main(string[] args) { // un tableau de personnes Personne[] personnes1 = { new Personne("claude", "pollon", 25), new Personne("valentine", "germain", 35), new Personne("paul", "germain", 32) }; // affichage Affiche("Tableau trier", personnes1); // tri selon le nom Array.Sort(personnes1, new CompareNoms()); // affichage Affiche("Tableau aprs le tri selon les nom et prnom", personnes1); // tri selon l'ge Array.Sort(personnes1, new CompareAges()); // affichage Affiche("Tableau aprs le tri selon l'ge", personnes1); } static void Affiche(string texte, Personne[] personnes) { Console.WriteLine(texte.PadRight(50, '-')); foreach (Personne p in personnes) { Console.WriteLine(p); } } } // classe de comparaison des noms et prnoms des personnes class CompareNoms : IComparer<Personne> { public int Compare(Personne p1, Personne p2) { // on compare les noms int i = p1.Nom.CompareTo(p2.Nom); if (i != 0) return i; // galit des noms - on compare les prnoms return p1.Prenom.CompareTo(p2.Prenom); } } // classe de comparaison des ages des personnes class CompareAges : IComparer<Personne> { public int Compare(Personne p1, Personne p2) { // on compare les ages if (p1.Age > p2.Age) return 1; else if (p1.Age == p2.Age) return 0; else return -1; } } }

ligne 8 : le tableau de personnes ligne 12 : le tri du tableau de personnes selon les nom et prnom. Le 2ime paramtre de la mthode gnrique Sort est une instance d'une classe CompareNoms implmentant l'interface gnrique IComparer<Personne>.

Classes, Stuctures, Interfaces

82

lignes 30-39 : la classe CompareNoms implmentant l'interface gnrique IComparer<Personne>. lignes 31-38 : implmentation de la mthode gnrique int CompareTo(T,T) de l'interface IComparer<T>. La mthode utilise la mthode String.CompareTo, prsente page 21, pour comparer deux chanes de caractres. ligne 16 : le tri du tableau de personnes selon les ges. Le 2ime paramtre de la mthode gnrique Sort est une instance d'une classe CompareAges implmentant l'interface gnrique IComparer<Personne> et dfinie lignes 42-51.

Les rsultats de l'excution sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. Tableau trier----------------------------------[claude, pollon, 25] [valentine, germain, 35] [paul, germain, 32] Tableau aprs le tri selon les nom et prnom-----[paul, germain, 32] [valentine, germain, 35] [claude, pollon, 25] Tableau aprs le tri selon l'ge-----------------[claude, pollon, 25] [paul, germain, 32] [valentine, germain, 35]

2.9

Les espaces de noms

Pour crire une ligne l'cran, nous utilisons l'instruction


Console.WriteLine(...)

Si nous regardons la dfinition de la classe Console


Namespace: System Assembly: Mscorlib (in Mscorlib.dll)

on dcouvre qu'elle fait partie de l'espace de noms System. Cela signifie que la classe Console devrait tre dsigne par System.Console et on devrait en fait crire :
System.Console.WriteLine(...)

On vite cela en utilisant une clause using :


using System; ... Console.WriteLine(...)

On dit qu'on importe l'espace de noms System avec la clause using. Lorsque le compilateur va rencontrer le nom d'une classe (ici Console) il va chercher la trouver dans les diffrents espaces de noms imports par les clauses using. Ici il trouvera la classe Console dans l'espace de noms System. Notons maintenant la seconde information attache la classe Console :
Assembly: Mscorlib (in Mscorlib.dll)

Cette ligne indique dans quelle "assemblage" se trouve la dfinition de la classe Console. Lorsqu'on compile en-dehors de Visual Studio et qu'on doit donner les rfrences des diffrentes dll contenant les classes que l'on doit utiliser, cette information peut s'avrer utile. Pour rfrencer les dll ncessaires la compilation d'une classe, on crit : csc /r:fic1.dll /r:fic2.dll ... prog.cs o csc est le compilateur C#. Lorsqu'on cre une classe, on peut la crer l'intrieur d'un espace de noms. Le but de ces espaces de noms est d'viter les conflits de noms entre classes lorsque celles-ci sont vendues par exemple. Considrons deux entreprises E1 et E2 distribuant des classes empaquetes respectivement dans les dll, e1.dll et e2.dll. Soit un client C qui achte ces deux ensembles de classes dans lesquelles les deux entreprises ont dfini toutes deux une classe Personne. Le client C compile un programme de la faon suivante : csc /r:e1.dll /r:e2.dll prog.cs

Classes, Stuctures, Interfaces

83

Si le source prog.cs utilise la classe Personne, le compilateur ne saura pas s'il doit prendre la classe Personne de e1.dll ou celle de e2.dll. Il signalera une erreur. Si l'entreprise E1 prend soin de crer ses classes dans un espace de noms appel E1 et l'entreprise E2 dans un espace de noms appel E2, les deux classes Personne s'appelleront alors E1.Personne et E2.Personne. Le client devra employer dans ses classes soit E1.Personne, soit E2.Personne mais pas Personne. L'espace de noms permet de lever l'ambigut. Pour crer une classe dans un espace de noms, on crit :
namespace EspaceDeNoms{ // dfinition de la classe }

2.10

Application exemple - V2

On reprend le calcul de l'impt dj tudi dans le chapitre prcdent page 31 et on le traite maintenant en utilisant des classes et des interfaces. Rappelons le problme : On se propose d'crire un programme permettant de calculer l'impt d'un contribuable. On se place dans le cas simplifi d'un contribuable n'ayant que son seul salaire dclarer (chiffres 2004 pour revenus 2003) :

on calcule le nombre de parts du salari nbParts=nbEnfants/2 +1 s'il n'est pas mari, nbEnfants/2+2 s'il est mari, o nbEnfants est son nombre d'enfants. s'il a au moins trois enfants, il a une demi part de plus on calcule son revenu imposable R=0.72*S o S est son salaire annuel on calcule son coefficient familial QF=R/nbParts on calcule son impt I. Considrons le tableau suivant :
4262 8382 14753 23888 38868 47932 0 0 0.0683 0.1914 0.2826 0.3738 0.4262 0.4809 0 291.09 1322.92 2668.39 4846.98 6883.66 9505.54

Chaque ligne a 3 champs. Pour calculer l'impt I, on recherche la premire ligne o QF<=champ1. Par exemple, si QF=5000 on trouvera la ligne 8382 0.0683 291.09 L'impt I est alors gal 0.0683*R - 291.09*nbParts. Si QF est tel que la relation QF<=champ1 n'est jamais vrifie, alors ce sont les coefficients de la dernire ligne qui sont utiliss. Ici : 0 0.4809 9505.54 ce qui donne l'impt I=0.4809*R - 9505.54*nbParts. Tout d'abord, nous dfinissons une structure capable d'encapsuler une ligne du tableau prcdent :
1. 2. 3. 4. 5. 6. 7. 8. 9. namespace Chap2 { // une tranche d'impt struct TrancheImpot { public decimal Limite { get; set; } public decimal CoeffR { get; set; } public decimal CoeffN { get; set; } } }

Puis nous dfinissons une interface IImpot capable de calculer l'impt :


1. 2. 3. 4. 5. namespace Chap2 { interface IImpot { int calculer(bool mari, int nbEnfants, int salaire); } }

ligne 3 : la mthode de calcul de l'impt partir de trois donnes : l'tat mari ou non du contribuable, son nombre d'enfants, son salaire

Classes, Stuctures, Interfaces

84

Ensuite, nous dfinissons une classe abstraite implmentant cette interface :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. namespace Chap2 { abstract class AbstractImpot : IImpot { // les tranches d'impt ncessaires au calcul de l'impt // proviennent d'une source extrieure protected TrancheImpot[] tranchesImpot; // calcul de l'impt public int calculer(bool mari, int nbEnfants, int salaire) { // calcul du nombre de parts decimal nbParts; if (mari) nbParts = (decimal)nbEnfants / 2 + 2; else nbParts = (decimal)nbEnfants / 2 + 1; if (nbEnfants >= 3) nbParts += 0.5M; // calcul revenu imposable & Quotient familial decimal revenu = 0.72M * salaire; decimal QF = revenu / nbParts; // calcul de l'impt tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1; int i = 0; while (QF > tranchesImpot[i].Limite) i++; // retour rsultat return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN); }//calculer }//classe }

ligne 2 : la classe AbstractImpot implmente l'interface IImpot. ligne 7 : les donnes annuelles du calcul de l'impt sous forme d'un champ protg. La classe AbstractImpot ne sait pas comment sera initialis ce champ. Elle en laisse le soin aux classes drives. C'est pourquoi elle est dclare abstraite (ligne 2) afin d'en interdire toute instanciation. lignes 10-25 : l'implmentation de la mthode calculer de l'interface IImpot. Les classes drives n'auront pas rcrire cette mthode. La classe AbstractImpot sert ainsi de classe de factorisation des classes drives. On y met ce qui est commun toutes les classes drives.

Une classe implmentant l'interface IImpot peut tre construite en drivant la classe AbstractImpot. C'est ce que nous faisons maintenant :
1. using System; 2. 3. namespace Chap2 { 4. class HardwiredImpot : AbstractImpot { 5. 6. // tableaux de donnes ncessaires au calcul de l'impt 7. decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M }; 8. decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M }; 9. decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M }; 10. 11. public HardwiredImpot() { 12. // cration du tableau des tranches d'impt 13. tranchesImpot = new TrancheImpot[limites.Length]; 14. // remplissage 15. for (int i = 0; i < tranchesImpot.Length; i++) { 16. tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] }; 17. } 18. } 19. }// classe 20. }// namespace

La classe HardwiredImpot dfinit, lignes 7-9, en dur les donnes ncessaires au calcul de l'impt. Son constructeur (lignes 11-18) utilise ces donnes pour initialiser le champ protg tranchesImpot de la classe mre AbstractImpot. Un programme de test pourait tre le suivant :
1. 2. 3. using System; namespace Chap2 {

Classes, Stuctures, Interfaces

85

4. class Program { 5. static void Main() { 6. // programme interactif de calcul d'Impot 7. // l'utilisateur tape trois donnes au clavier : mari nbEnfants salaire 8. // le programme affiche alors l'Impot payer 9. 10. const string syntaxe = "syntaxe : Mari NbEnfants Salaire\n" 11. + "Mari : o pour mari, n pour non mari\n" 12. + "NbEnfants : nombre d'enfants\n" 13. + "Salaire : salaire annuel en F"; 14. 15. // cration d'un objet IImpot 16. IImpot impot = new HardwiredImpot(); 17. 18. // boucle infinie 19. while (true) { 20. // on demande les paramtres du calcul de l'impt 21. Console.Write("Paramtres du calcul de l'Impot au format : Mari (o/n) NbEnfants Salaire ou rien pour arrter :"); 22. string paramtres = Console.ReadLine().Trim(); 23. // qq chose faire ? 24. if (paramtres == null || paramtres == "") break; 25. // vrification du nombre d'arguments dans la ligne saisie 26. string[] args = paramtres.Split(null); 27. int nbParamtres = args.Length; 28. if (nbParamtres != 3) { 29. Console.WriteLine(syntaxe); 30. continue; 31. }//if 32. // vrification de la validit des paramtres 33. // mari 34. string mari = args[0].ToLower(); 35. if (mari != "o" && mari != "n") { 36. Console.WriteLine(syntaxe + "\nArgument mari incorrect : tapez o ou n"); 37. continue; 38. }//if 39. // nbEnfants 40. int nbEnfants = 0; 41. bool dataOk = false; 42. try { 43. nbEnfants = int.Parse(args[1]); 44. dataOk = nbEnfants >= 0; 45. } catch { 46. }//if 47. // donne correcte ? 48. if (!dataOk) { 49. Console.WriteLine(syntaxe + "\nArgument NbEnfants incorrect : tapez un entier positif ou nul"); 50. continue; 51. } 52. // salaire 53. int salaire = 0; 54. dataOk = false; 55. try { 56. salaire = int.Parse(args[2]); 57. dataOk = salaire >= 0; 58. } catch { 59. }//try-catch 60. // donne correcte ? 61. if (!dataOk) { 62. Console.WriteLine(syntaxe + "\nArgument salaire incorrect : tapez un entier positif ou nul"); 63. continue; 64. } 65. // les paramtres sont corrects - on calcule l'Impot 66. Console.WriteLine("Impot=" + impot.calculer(mari == "o", nbEnfants, salaire) + " euros"); 67. // contribuable suivant 68. }//while 69. } 70. } 71. }

Le programme ci-dessus permet l'utilisateur de faire des simulations rptes de calcul d'impt.

Classes, Stuctures, Interfaces

86

ligne 16 : cration d'un objet impot implmentant l'interface IImpot. Cet objet est obtenu par instanciation d'un type HardwiredImpot, un type qui implmente l'interface IImpot. On notera qu'on n'a pas donn la variable impot, le type HardwiredImpot mais le type IImpot. En crivant cela, on indique qu'on ne s'intresse qu' la mthode calculer de l'objet impot et pas au reste. lignes 19-68 : la boucle des simulations de calcul de l'impt ligne 22 : les trois paramtres ncessaires la mthode calculer sont demands en une seule ligne tape au clavier. ligne 26 : la mthode [chaine].Split(null) permet de dcomposer [chaine] en mots. Ceux-ci sont stocks dans un tableau args. ligne 66 : appel de la mthode calculer de l'objet impot implmentant l'interface IImpot.

Voici un exemple d'excution du programme :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. Paramtres du calcul de l'Impot au format : Mari (o/n) s d syntaxe : Mari NbEnfants Salaire Mari : o pour mari, n pour non mari NbEnfants : nombre d'enfants Salaire : salaire annuel en euros Argument mari incorrect : tapez o ou n Paramtres du calcul de l'Impot au format : Mari (o/n) 2 d syntaxe : Mari NbEnfants Salaire Mari : o pour mari, n pour non mari NbEnfants : nombre d'enfants Salaire : salaire annuel en euros Argument salaire incorrect : tapez un entier positif ou Paramtres du calcul de l'Impot au format : Mari (o/n) s d f syntaxe : Mari NbEnfants Salaire Mari : o pour mari, n pour non mari NbEnfants : nombre d'enfants Salaire : salaire annuel en euros Paramtres du calcul de l'Impot au format : Mari (o/n) 2 60000 Impot=4282 euros NbEnfants Salaire ou rien pour arrter :q

NbEnfants Salaire ou rien pour arrter :o

nul NbEnfants Salaire ou rien pour arrter :q

NbEnfants Salaire ou rien pour arrter :o

Classes, Stuctures, Interfaces

87

Classes .NET d'usage courant

Nous prsentons ici quelques classes de la plate-forme .NET frquemment utilises. Auparavant, nous montrons comment obtenir des renseignements sur les quelques centaines de classes disponibles. Cette aide est indispensable au dvelopeur C# mme confirm. Le niveau de qualit d'une aide (accs facile, organisation comprhensible, pertinence des informations, ...) peut faire le succs ou l'chec d'un environnement de dveloppement.

3.1

Chercher de l'aide sur les classes .NET

Nous donnons ici quelques indications pour trouver de l'aide avec Visual Studio.NET

3.1.1

Help/Contents

2 4 1 3

en [1], prendre l'option Help/Contents du menu. en [2], prendre l'option Visual C# Express Edition en [3], l'arbre de l'aide sur C# en [4], une autre option utile est .NET Framework qui donne accs toutes les classes du framework .NET.

Faisons le tour des ttes de chapitre de l'aide C# :

Classes .NET d'usage courant

88

3 2

[1] : une vue d'ensemble de C# [2] : une srie d'exemples sur certains points de C# [3] : un cours C# - pourrait remplacer avantageusement le prsent document... 4 5 6

[4] : pour aller dans les dtails de C# [5] : utile pour les dveloppeurs C++ ou Java. Permet d'viter quelques piges. [6] : lorsque vous cherchez des exemples, vous pouvez commencer par l.

Classes .NET d'usage courant

89

[7] : ce qu'il faut savoir pour crer des interfaces graphiques [8] : pour mieux utiliser l'IDE Visual Studio Express 9

[9] : SQL Server Express 2005 est un SGBD de qualit distribu gratuitement. Nous l'utiliserons dans ce cours.

L'aide C# n'est qu'une partie de ce dont a besoin le dveloppeur. L'autre partie est l'aide sur les centaines de classes du framework .NET qui vont lui faciliter son travail. 1 3 4

[1] : on slectionne l'aide sur le framework .NET [2] : l'aide se trouve dans la branche .NET Framework SDK [3] : la branche .NET Framework Class Library prsente toutes les classes .NET selon l'espace de noms auquel elles appartiennent [4] : l'espace de noms System qui a t le plus souvent utilis dans les exemples des chapitres prcdents

Classes .NET d'usage courant

90

[5] : dans l'espace de noms System, un exemple, ici la structure DateTime

[6] : l'aide sur la structure DateTime

3.1.2

Help/Index/Search

L'aide fournie par MSDN est immense et on peut ne pas savoir o chercher. On peut alors utiliser l'index de l'aide : 2

3 4

1 5

en [1], utiliser l'option [Help/Index] si la fentre d'aide n'est pas dj ouverte, sinon utiliser [2] dans une fentre d'aide existante. en [3], prciser le domaine dans lequel doit se faire la recherche en [4], prciser ce que vous cherchez, ici une classe

Classes .NET d'usage courant

91

en [5], la rponse

Une autre faon de chercher de l'aide est d'utiliser la fonction search de l'aide : 2 4 1

en [1], utiliser l'option [Help/Search] si la fentre d'aide n'est pas dj ouverte, sinon utiliser [2] dans une fentre d'aide existante. en [3], prciser ce qui est cherch en [4], filtrer les domaines de recherche

en [5], la rponse sous forme de diffrents thmes o le texte cherch a t trouv.

3.2
3.2.1

Les chanes de caractres


La classe System.String

Classes .NET d'usage courant

92

La classe System.String est identique au type simple string. Elle prsente de nombreuses proprits et mthodes. En voici quelques-unes :
public int Length { get; } public bool EndsWith(string value) public bool StartsWith(string value) public virtual bool Equals(object obj) public int IndexOf(string value, int startIndex) nombre de caractres de la chane rend vrai si la chane se termine par value rend vrai si la chane commence par value rend vrai si la chanes est gale obj - quivalent chane==obj rend la premire position dans la chane de la chane value - la recherche commence partir du caractre n startIndex idem mais pour le caractre value insre la chane value dans chane en position startIndex mthode de classe - rend une chane de caractres, rsultat de la concatnation des valeurs du tableau value avec le sparateur separator idem indexOf mais rend la dernire position au lieu de la premire

public int IndexOf(char value, int startIndex) public string Insert(int startIndex, string value) public static string Join(string separator, string[] value) public int LastIndexOf(char value, int startIndex, int count) public int LastIndexOf(string value, int startIndex, int count) public string Replace(char oldChar, char newChar)

rend une chane copie de la chane courante o le caractre oldChar a t remplac par le caractre newChar la chane est vue comme une suite de champs spars par les caractres prsents dans le tableau separator. Le rsultat est le tableau de ces champs

public string[] Split(char[] separator)

public string Substring(int startIndex, int length) sous-chane de la chane courante commenant la position startIndex et ayant length caractres public string ToLower() public string ToUpper() public string Trim() rend la chane courante en minuscules rend la chane courante en majuscules rend la chane courante dbarrasse de ses espaces de dbut et fin

On notera un point important : lorsqu'une mthode rend une chane de caractres, celle-ci est une chane diffrente de la chane sur laquelle a t applique la mthode. Ainsi S1.Trim() rend une chane S2, et S1 et S2 sont deux chanes diffrentes. Une chane C peut tre considre comme un tableau de caractres. Ainsi C[i] est le caractre i de C C.Length est le nombre de caractres de C Considrons l'exemple suivant :
1. using System; 2. 3. namespace Chap3 { 4. class Program { 5. static void Main(string[] args) { 6. string uneChaine = "l'oiseau vole au-dessus des nuages"; 7. affiche("uneChaine=" + uneChaine); 8. affiche("uneChaine.Length=" + uneChaine.Length); 9. affiche("chaine[10]=" + uneChaine[10]); 10. affiche("uneChaine.IndexOf(\"vole\")=" + uneChaine.IndexOf("vole")); 11. affiche("uneChaine.IndexOf(\"x\")=" + uneChaine.IndexOf("x")); 12. affiche("uneChaine.LastIndexOf('a')=" + uneChaine.LastIndexOf('a')); 13. affiche("uneChaine.LastIndexOf('x')=" + uneChaine.LastIndexOf('x')); 14. affiche("uneChaine.Substring(4,7)=" + uneChaine.Substring(4, 7)); 15. affiche("uneChaine.ToUpper()=" + uneChaine.ToUpper()); 16. affiche("uneChaine.ToLower()=" + uneChaine.ToLower()); 17. affiche("uneChaine.Replace('a','A')=" + uneChaine.Replace('a', 'A')); 18. string[] champs = uneChaine.Split(null); 19. for (int i = 0; i < champs.Length; i++) { 20. affiche("champs[" + i + "]=[" + champs[i] + "]"); 21. }//for 22. affiche("Join(\":\",champs)=" + System.String.Join(":", champs));

Classes .NET d'usage courant

93

23. affiche("(\" abc \").Trim()=[" + " abc 24. }//Main 25. 26. public static void affiche(string msg) { 27. // affiche msg 28. Console.WriteLine(msg); 29. }//affiche 30. }//classe 31. }//namespace

".Trim() + "]");

L'excution donne les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. uneChaine=l'oiseau vole au-dessus des nuages uneChaine.Length=34 chaine[10]=o uneChaine.IndexOf("vole")=9 uneChaine.IndexOf("x")=-1 uneChaine.LastIndexOf('a')=30 uneChaine.LastIndexOf('x')=-1 uneChaine.Substring(4,7)=seau vo uneChaine.ToUpper()=L'OISEAU VOLE AU-DESSUS DES NUAGES uneChaine.ToLower()=l'oiseau vole au-dessus des nuages uneChaine.Replace('a','A')=l'oiseAu vole Au-dessus des nuAges champs[0]=[l'oiseau] champs[1]=[vole] champs[2]=[au-dessus] champs[3]=[des] champs[4]=[nuages] Join(":",champs)=l'oiseau:vole:au-dessus:des:nuages (" abc ").Trim()=[abc]

Considrons un nouvel exemple :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System; namespace Chap3 { class Program { static void Main(string[] args) { // la ligne analyser string ligne = "un:deux::trois:"; // les sparateurs de champs char[] sparateurs = new char[] { ':' }; // split string[] champs = ligne.Split(sparateurs); for (int i = 0; i < champs.Length; i++) { Console.WriteLine("Champs[" + i + "]=" + champs[i]); } // join Console.WriteLine("join=[" + System.String.Join(":", champs) + "]"); } } }

et les rsultats d'excution :


1. 2. 3. 4. 5. 6. Champs[0]=un Champs[1]=deux Champs[2]= Champs[3]=trois Champs[4]= join=[un:deux::trois:]

La mthode Split de la classe String permet de mettre dans un tableau des lments d'une chane de caractres. La dfinition de la mthode Split utilise ici est la suivante :
public string[] Split(char[] separator); separator

rsultat

tableau de caractres. Ces caractres reprsentent les caractres utiliss pour sparer les champs de la chane de caractres. Ainsi si la chane est "champ1, champ2, champ3" on pourra utiliser separator=new char[] {','}. Si le sparateur est une suite d'espaces on utilisera separator=null. tableau de chanes de caractres o chaque lment du tableau est un champ de la chane.

La mthode Join est une mthode statique de la classe String :

Classes .NET d'usage courant

94

public static string Join(string separator, string[] value); value separator rsultat

tableau de chanes de caractres une chane de caractres qui servira de sparateur de champs une chane de caractres forme de la concatnation des lments du tableau value spars par la chane separator.

3.2.2

La classe System.Text.StringBuilder

Prcdemment, nous avons dit que les mthodes de la classe String qui s'appliquaient une chane de caractres S1 rendait une autre chane S2. La classe System.Text.StringBuilder permet de manipuler S1 sans avoir crer une chane S2. Cela amliore les performances en vitant la multiplication de chanes dure de vie trs limite. La classe admet divers constructeurs :
StringBuilder() StringBuilder(String value) constructeur par dfaut construction et initialisation avec value

StringBuilder(String value, int capacit) construction et initialisation avec value avec une taille de bloc de capacit caractres.

Un objet StringBuilder travaille avec des blocs de capacit caractres pour stocker la chane sous-jacente. Par dfaut capacit vaut 16. Le 3ime constructeur ci-dessus permet de prciser la capacit des blocs. Le nombre de blocs de capacit caractres ncessaire pour stocker une chane S est ajust automatiquement par la classe StringBuilder. Il existe des constructeurs pour fixer le nombre maximal de caractres dans un objet StringBuilder. Par dfaut, cette capacit maximale est 2 147 483 647. Voici un exemple illustrant cette notion de capacit :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using System.Text; using System; namespace Chap3 { class Program { static void Main(string[] args) { // str StringBuilder str = new StringBuilder("test"); Console.WriteLine("taille={0}, capacit={1}", str.Length, str.Capacity); for (int i = 0; i < 10; i++) { str.Append("test"); Console.WriteLine("taille={0}, capacit={1}", str.Length, str.Capacity); } // str2 StringBuilder str2 = new StringBuilder("test",10); Console.WriteLine("taille={0}, capacit={1}", str2.Length, str2.Capacity); for (int i = 0; i < 10; i++) {

Classes .NET d'usage courant

95

17. 18. 19. 20. 21. } 22. }

} }

str2.Append("test"); Console.WriteLine("taille={0}, capacit={1}", str2.Length, str2.Capacity);

ligne 7 : cration d'un objet StringBuilder avec une taille de bloc de 16 caractres ligne 8 : str.Length est le nombre actuel de caractres de la chane str. str.Capacity est le nombre de caractres que peut stocker la chane str actuelle avant rallocation d'un nouveau bloc. ligne 10 : str.Append(String S) permet de concatner la chane S de type String la chane str de type StringBuilder. ligne 14 : cration d'un objet StringBuilder avec une capacit de bloc de 10 caractres

Le rsultat de l'excution :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. taille=4, capacit=16 taille=8, capacit=16 taille=12, capacit=16 taille=16, capacit=16 taille=20, capacit=32 taille=24, capacit=32 taille=28, capacit=32 taille=32, capacit=32 taille=36, capacit=64 taille=40, capacit=64 taille=44, capacit=64 taille=4, capacit=10 taille=8, capacit=10 taille=12, capacit=20 taille=16, capacit=20 taille=20, capacit=20 taille=24, capacit=40 taille=28, capacit=40 taille=32, capacit=40 taille=36, capacit=40 taille=40, capacit=40 taille=44, capacit=80

Ces rsultats montrent que la classe suit un algorithme qui lui est propre pour allouer de nouveaux blocs lorsque sa capacit est insuffisante :

lignes 4-5 : augmentation de la capacit de 16 caractres lignes 8-9 : augmentation de la capacit de 32 caractres alors que 16 auraient suffi.

Voici quelques-unes des mthodes de la classe :


public StringBuilder Append(string value) ajoute la chane value l'objet StringBuilder. Rend l'objet StringBuilder. Cette mthode est surcharge pour admettre diffrents types pour value : byte, int, float, double, decimal, ... insre value la position index. Cette mthode est surcharge comme la prcdente pour accepter diffrents types pour value. supprime length caractres partir de la position index. remplace dans StringBuilder, la chane oldValue par la chane newValue. Il existe une version surcharge (char oldChar, char newChar). convertit l'objet StringBuilder en un objet de type String.

public StringBuilder Insert(int index, string value) public StringBuilder Remove(int index, int length) public StringBuilder Replace(string oldValue, string newValue) public String ToString()

Voici un exemple :
1. 2. 3. 4. 5. 6. using System.Text; using System; namespace Chap3 { class Program { static void Main(string[] args) { // str3

Classes .NET d'usage courant

96

7. 8. 9. 10. } 11. }

StringBuilder str3 = new StringBuilder("test"); Console.WriteLine(str3.Append("abCD").Insert(2, "xyZT").Remove(0, 2).Replace("xy", "XY"));

et ses rsultats :
XYZTstabCD

3.3

Les tableaux

Les tableaux drivent de la classe Array :

La classe Array possde diverses mthodes pour trier un tableau, rechercher un lment dans un tableau, redimensionner un tableau, ... Nous prsentons certaines proprits et mthodes de cette classe. Elles sont quasiment toutes surcharges, c.a.d. qu'elles existent en diffrentes variantes. Tout tableau en hrite. Proprits
public int Length {get;} nombre total d'lments du tableau, quelque soit son nombre de dimensions public int Rank {get;} nombre total de dimensions du tableau

Mthodes
public static int BinarySearch<T>(T[] tableau,T value) public static int BinarySearch<T>(T[] tableau,int index, int length, T value) public static void Clear(Array tableau, int index, int length) public static void Copy(Array source, Array destination, int length) public int GetLength(int i) public int GetLowerBound(int i) public int GetUpperBound(int i) rend la position de value dans tableau. idem mais cherche dans tableau partir de la position index et sur length lments met les length lments de tableau commenant au n index 0 si numriques, false si boolens, null si rfrences copie length lments de source dans destination nombre d'lments de la dimension n i du tableau indice du 1er lment de la dimension n i indice du dernier lment de la dimension n i

public static int IndexOf<T>(T[] tableau, T valeur) rend la position de valeur dans tableau ou -1 si valeur n'est pas trouve. public static void Resize<T>(ref T[] tableau, int n) public static void Sort<T>(T[] tableau, IComparer<T> comparateur) redimensionne tableau n lments. Les lments dj prsents sont conservs. trie tableau selon un ordre dfini par comparateur. Cette mthode a t prsente page 81.

Classes .NET d'usage courant

97

Le programme suivant illustre l'utilisation de certaines mthodes de la classe Array :


1. using System; 2. 3. namespace Chap3 { 4. class Program { 5. // type de recherche 6. enum TypeRecherche { linaire, dichotomique }; 7. 8. // mthode principale 9. static void Main(string[] args) { 10. // lecture des lments d'un tableau taps au clavier 11. double[] lments; 12. Saisie(out lments); 13. // affichage tableau non tri 14. Affiche("Tableau non tri", lments); 15. // Recherche linaire dans le tableau non tri 16. Recherche(lments, TypeRecherche.linaire); 17. // tri du tableau 18. Array.Sort(lments); 19. // affichage tableau tri 20. Affiche("Tableau tri", lments); 21. // Recherche dichotomique dans le tableau tri 22. Recherche(lments, TypeRecherche.dichotomique); 23. } 24. 25. // saisie des valeurs du tableau lments 26. // lments : rfrence sur tableau cr par la mthode 27. static void Saisie(out double[] lments) { 28. bool termin = false; 29. string rponse; 30. bool erreur; 31. double lment = 0; 32. int i = 0; 33. // au dpart, le tableau n'existe pas 34. lments = null; 35. // boucle de saisie des lments du tableau 36. while (!termin) { 37. // question 38. Console.Write("Elment (rel) " + i + " du tableau (rien pour terminer) : "); 39. // lecture de la rponse 40. rponse = Console.ReadLine().Trim(); 41. // fin de saisie si chane vide 42. if (rponse.Equals("")) 43. break; 44. // vrification saisie 45. try { 46. lment = Double.Parse(rponse); 47. erreur = false; 48. } catch { 49. Console.Error.WriteLine("Saisie incorrecte, recommencez"); 50. erreur = true; 51. }//try-catch 52. // si pas d'erreur 53. if (!erreur) { 54. // un lment de plus dans le tableau 55. i += 1; 56. // redimensionnement tableau pour accueillir le nouvel lment 57. Array.Resize(ref lments, i); 58. // insertion nouvel lment 59. lments[i - 1] = lment; 60. } 61. }//while 62. } 63. 64. // mthode gnrique pour afficher les lments d'un tableau 65. static void Affiche<T>(string texte, T[] lments) { 66. Console.WriteLine(texte.PadRight(50, '-')); 67. foreach (T lment in lments) { 68. Console.WriteLine(lment); 69. } 70. } 71. 72. // recherche d'un lment dans le tableau 73. // lments : tableau de rels 74. // TypeRecherche : dichotomique ou linaire

Classes .NET d'usage courant

98

75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112. 113. 114. 115. } 116.}

static void Recherche(double[] lments, TypeRecherche type) { // Recherche bool termin = false; string rponse = null; double lment = 0; bool erreur = false; int i = 0; while (!termin) { // question Console.WriteLine("Elment cherch (rien pour arrter) : "); // lecture-vrification rponse rponse = Console.ReadLine().Trim(); // fini ? if (rponse.Equals("")) break; // vrification try { lment = Double.Parse(rponse); erreur = false; } catch { Console.WriteLine("Erreur, recommencez..."); erreur = true; }//try-catch // si pas d'erreur if (!erreur) { // on cherche l'lment dans le tableau if (type == TypeRecherche.dichotomique) // recherche dichotomique i = Array.BinarySearch(lments, lment); else // recherche linaire i = Array.IndexOf(lments, lment); // Affichage rponse if (i >= 0) Console.WriteLine("Trouv en position " + i); else Console.WriteLine("Pas dans le tableau"); }//if }//while }

lignes 27-62 : la mthode Saisie saisit les lments d'un tableau lments taps au clavier. Comme on ne peut dimensionner le tableau priori (on ne connat pas sa taille finale), on est obligs de le redimensionner chaque nouvel lment (ligne 57). Un algorithme plus efficace aurait t d'allouer de la place au tableau par groupe de N lments. Un tableau n'est cependant pas fait pour tre redimensionn . Ce cas l est mieux trait avec une liste (ArrayList, List<T>). lignes 75-113 : la mthode Recherche permet de rechercher dans le tabeau lments, un lment tap au clavier. Le mode de recherche est diffrent selon que le tableau est tri ou non. Pour un tableau non tri, on fait une recherche linaire avec la mthode IndexOf de la ligne 106. Pour un tableau tri, on fait une recherche dichotomique avec la mthode BinarySearch de la ligne 103. ligne 18 : on trie le tableau lments. On utilise ici, une variante de Sort qui n'a qu'un paramtre : le tableau trier. La relation d'ordre utilise pour comparer les lments du tableau est alors celle implicite de ces lments. Ici, les lments sont numriques. C'est l'ordre naturel des nombres qui est utilis.

Les rsultats cran sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. Elment (rel) 0 du tableau (rien pour terminer) : Elment (rel) 1 du tableau (rien pour terminer) : Elment (rel) 2 du tableau (rien pour terminer) : Elment (rel) 3 du tableau (rien pour terminer) : Elment (rel) 4 du tableau (rien pour terminer) : Tableau non tri---------------------------------3,6 7,4 -1,5 -7 Elment cherch (rien pour arrter) : 7,4 Trouv en position 1 Elment cherch (rien pour arrter) : 0 Pas dans le tableau Elment cherch (rien pour arrter) : 3,6 7,4 -1,5 -7

Classes .NET d'usage courant

99

18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30.

Tableau tri--------------------------------------7 -1,5 3,6 7,4 Elment cherch (rien pour arrter) : 7,4 Trouv en position 3 Elment cherch (rien pour arrter) : 0 Pas dans le tableau Elment cherch (rien pour arrter) :

3.4

Les collections gnriques

Outre le tableau, il existe diverses classes pour stocker des collections d'lments. Il existe des versions gnriques dans l'espace de noms System.Collections.Generic et des versions non gnriques dans System.Collections. Nous prsenterons deux collections gnriques frquemment utilises : la liste et le dictionnaire. La liste des collections gnriques est la suivante :

3.4.1

La classe gnrique List<T>

La classe System.Collections.Generic.List<T> permet d'implmenter des collections d'objets de type T dont la taille varie au cours de l'excution du programme. Un objet de type List<T> se manipule presque comme un tableau. Ainsi l'lment i d'une liste l est-il not l[i]. Il existe galement un type de liste non gnrique : ArrayList capable de stocker des rfrences sur des objets quelconques. ArrayList est fonctionnellement quivalente List<Object>. Un objet ArrayList ressemble ceci : 0 1 i

Classes .NET d'usage courant

100

Ci-dessus, les lments 0, 1 et i de la liste pointent sur des objets de types diffrents. Il faut qu'un objet soit d'abord cr avant d'ajouter sa rfrence la liste ArrayList. Bien qu'un ArrayList stocke des rfrences d'objet, il est possible d'y stocker des nombres. Cela se fait par un mcanisme appel Boxing : le nombre est encapsul dans un objet O de type Object et c'est la rfrence O qui est stock dans la liste. C'est un mcanisme transparent pour le dveloppeur. On peut ainsi crire :
ArrayList liste=new ArrayList(); liste.Add(4);

Cela produira le rsultat suivant : 0 1 i

Ci-dessus, le nombre 4 a t encapsul dans un objet O et la rfrence O est mmorise dans la liste. Pour le rcuprer, on pourra crire :
int i = (int)liste[0];

L'opration Object -> int est appele Unboxing. Si une liste est entirement compose de types int, la dclarer comme List<int> amliore les performances. En effet, les nombres de type int sont alors stocks dans la liste elle-mme et non dans des types Object extrieurs la liste. Les oprations Boxing / Unboxing n'ont plus lieu.

100

17

45

Pour un objet List<T> ou T est une classe, la liste stocke l encore les rfrences des objets de type T : 0 1 i

Voici quelques-unes des proprits et mthodes des listes gnriques : Proprits


public int Count {get;} nombre d'lments de la liste

public int Capacity {get;} nombre d'lments que la liste peut contenir avant d'tre redimensionne. Ce redimensionnement se fait automatiquement. Cette notion de capacit de liste est analogue celle de capacit dcrite pour la classe StringBuilder page 95.

Mthodes

Classes .NET d'usage courant

101

public void Add(T item) public int BinarySearch<T>(T item) public int BinarySearch<T>(T item, IComparer<T> comparateur) public void Clear() public bool Contains(T item) public void CopyTo(T[] tableau) public int IndexOf(T item) public void Insert(T item, int index) public bool Remove(T item) public void RemoveAt(int index) public void Sort(IComparer<T> comparateur) public void Sort() public T[] ToArray()

ajoute item la liste rend la position de item dans la liste s'il s'y trouve sinon un nombre <0 idem mais le 2ime paramtre permet de comparer deux lments de la liste. L'interface IComparer<T> a t prsente page 81. supprime tous les lments de la liste rend True si item est dans la liste, False sinon copie les lments de la liste dans tableau. rend la position de item dans tableau ou -1 si valeur n'est pas trouve. insre item la position index de la liste supprime item de la liste. Rend True si l'opration russit, False sinon. supprime l'lment n index de la liste trie la liste selon un ordre dfini par comparateur. Cette mthode a t prsente page 81. trie la liste selon l'ordre dfini par le type des lments de la liste rend les lments de la liste sous forme de tableau

Reprenons l'exemple trait prcdemment avec un objet de type Array et traitons-le maintenant avec un objet de type List<T>. Parce que la liste est un objet proche du tableau, le code change peu. Nous ne prsentons que les modifications notables :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. using System; using System.Collections.Generic; namespace Chap3 { class Program { // type de recherche enum TypeRecherche { linaire, dichotomique }; // mthode principale static void Main(string[] args) { // lecture des lments d'une liste taps au clavier List<double> lments; Saisie(out lments); // nombre d'lments Console.WriteLine("La liste a {0} lments et une capacit de {1} lments", lments.Count, lments.Capacity); // affichage liste non trie Affiche("Liste non trie", lments); // Recherche linaire dans la liste non trie Recherche(lments, TypeRecherche.linaire); // tri de la liste lments.Sort(); // affichage liste trie Affiche("Liste trie", lments); // Recherche dichotomique dans la liste trie Recherche(lments, TypeRecherche.dichotomique); }

16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. // saisie des valeurs de la liste lments 29. // lments : rfrence sur la liste cre par la mthode 30. static void Saisie(out List<double> lments) { 31. ... 32. // au dpart, la liste est vide 33. lments = new List<double>(); 34. // boucle de saisie des lments de la liste 35. while (!termin) { 36. ... 37. // si pas d'erreur 38. if (!erreur) { 39. // un lment de plus dans la liste 40. lments.Add(lment); 41. } 42. }//while 43. } 44.

Classes .NET d'usage courant

102

45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75.

// mthode gnrique pour afficher les lments d'un objet numrable static void Affiche<T>(string texte, IEnumerable<T> lments) { Console.WriteLine(texte.PadRight(50, '-')); foreach (T lment in lments) { Console.WriteLine(lment); } } // recherche d'un lment dans la liste // lments : liste de rels // TypeRecherche : dichotomique ou linaire static void Recherche(List<double> lments, TypeRecherche type) { while (!termin) { // si pas d'erreur if (!erreur) { // on cherche l'lment dans la liste if (type == TypeRecherche.dichotomique) // recherche dichotomique i = lments.BinarySearch(lment); else // recherche linaire i = lments.IndexOf(lment); // Affichage rponse ... } } } }//if }//while

... ...

lignes 46-51 : la mthode gnrique Affiche<T> admet deux paramtres : le 1er paramtre est un texte crire le 2ime paramtre est un objet implmentant l'interface gnrique IEnumerable<T> :
public interface IEnumerable<T>{ IEnumerator GetEnumerator(); IEnumerator<T> GetEnumerator(); }

1. 2. 3. 4.

La structure foreach( T lment in lments) de la ligne 48, est valide pour tout objet lments implmentant l'interface IEnumerable. Les tableaux (Array) et les listes (List<T>) implmentent l'interface IEnumerable<T>. Aussi la mthode Affiche convient-elle aussi bien pour afficher des tableaux que des listes. Les rsultats d'excution du programme sont les mmes que dans l'exemple utilisant la classe Array.

3.4.2

La classe Dictionary<TKey,TValue>

La classe System.Collections.Generic.Dictionary<TKey,TValue> permet d'implmenter un dictionnaire. On peut voir un dictionnaire comme un tableau deux colonnes : cl cl1 cl2 .. valeur valeur1 valeur2 ...

Dans la classe Dictionary<TKey,TValue> les cls sont de type Tkey, les valeurs de type TValue. Les cls sont uniques, c.a.d. qu'il ne peut y avoir deux cls identiques. Un tel dictionnaire pourrait ressembler ceci si les types TKey et TValue dsignaient des classes :

Classes .NET d'usage courant

103

CLES

VALEURS

La valeur associe la cl C d'un dictionnaire D est obtenue par la notation D[C]. Cette valeur est en lecture et criture. Ainsi on peut crire :
1. 2. 3. 4. 5. TValue v=...; TKey c=...; Dictionary<TKey,TValue> D=new Dictionary<TKey,TValue>(); D[c]=v; v=D[c];

Si la cl c n'existe pas dans le dictionnaire D, la notation D[c] lance une exception. Les mthodes et proprits principales de la classe Dictionary<TKey,TValue> sont les suivantes : Constructeurs
public Dictionary<TKey,TValue>() constructeur sans paramtres - construit un dictionnaire vide. Il existe plusieurs autres constructeurs.

Proprits
public int Count {get;} public Dictionary<TKey,TValue>.KeyCollection Keys {get;} public Dictionary<TKey,TValue>.ValueCollection Values {get;} nombre d'entres (cl, valeur) dans le dictionnaire collection des cls du dictionnaire. collection des valeurs du dictionnaire.

Mthodes
public void Add(TKey key, TValue value) public void Clear() public bool ContainsKey (TKey key) public bool ContainsValue (TValue value) public void CopyTo(T[] tableau) public bool Remove(TKey key) public bool TryGetValue(TKey key,out TValue value) ajoute le couple (key, value) au dictionnaire supprime tous les couples du dictionnaire rend True si key est une cl du dictionnaire, False sinon rend True si value est une valeur du dictionnaire, False sinon copie les lments de la liste dans tableau. supprime du dictionnaire le couple de cl key. Rend True si l'opration russit, False sinon. rend dans value, la valeur associe la cl key si cette dernire existe, sinon rend la valeur par dfaut du type TValue (0 pour les nombres, false pour les boolens, null pour les rfrences d'objet)

Considrons le programme exemple suivant :


1. using System;

Classes .NET d'usage courant

104

2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62.

using System.Collections.Generic; namespace Chap3 { class Program { static void Main(string[] args) { // cration d'un dictionnaire <string,int> string[] liste = { "jean:20", "paul:18", "mlanie:10", "violette:15" }; string[] champs = null; char[] sparateurs = new char[] { ':' }; Dictionary<string,int> dico = new Dictionary<string,int>(); for (int i = 0; i <liste.Length; i++) { champs = liste[i].Split(sparateurs); dico[champs[0]]= int.Parse(champs[1]); }//for // nbre d'lments dans le dictionnaire Console.WriteLine("Le dictionnaire a " + dico.Count + " lments"); // liste des cls Affiche("[Liste des cls]",dico.Keys); // liste des valeurs Affiche("[Liste des valeurs]", dico.Values); // liste des cls & valeurs Console.WriteLine("[Liste des cls & valeurs]"); foreach (string cl in dico.Keys) { Console.WriteLine("cl=" + cl + " valeur=" + dico[cl]); } // on supprime la cl "paul" Console.WriteLine("[Suppression d'une cl]"); dico.Remove("paul"); // liste des cls & valeurs Console.WriteLine("[Liste des cls & valeurs]"); foreach (string cl in dico.Keys) { Console.WriteLine("cl=" + cl + " valeur=" + dico[cl]); } // recherche dans le dictionnaire String nomCherch = null; Console.Write("Nom recherch (rien pour arrter) : "); nomCherch = Console.ReadLine().Trim(); int value; while (!nomCherch.Equals("")) { dico.TryGetValue(nomCherch, out value); if (value!=0) { Console.WriteLine(nomCherch + "," + value); } else { Console.WriteLine("Nom " + nomCherch + " inconnu"); } // recherche suivante Console.Out.Write("Nom recherch (rien pour arrter) : "); nomCherch = Console.ReadLine().Trim(); }//while } // mthode gnrique pour afficher les lments d'un type numrable static void Affiche<T>(string texte, IEnumerable<T> lments) { Console.WriteLine(texte.PadRight(50, '-')); foreach (T lment in lments) { Console.WriteLine(lment); } } } }

ligne 8 : un tableau de string qui va servir initialiser le dictionnaire <string,int> ligne 11 : le dictionnaire <string,int> lignes 12-15 : son initialisation partir du tableau de string de la ligne 8 ligne 17 : nombre d'entres du dictionnaire ligne 19 : les cls du dictionnaire ligne 21 : les valeurs du dictionnaire ligne 29 : suppression d'une entre du dictionnaire ligne 41 : recherche d'une cl dans le dictionnaire. Si elle n'existe pas, la mthode TryGetValue mettra 0 dans value, car value est de type numrique. Cette technique n'est utilisable ici que parce qu'on sait que la valeur 0 n'est pas dans le dictionnaire.

Les rsultats d'excution sont les suivants :

Classes .NET d'usage courant

105

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25.

Le dictionnaire a 4 lments [Liste des cls]---------------------------------jean paul mlanie violette [Liste des valeurs]------------------------------20 18 10 15 [Liste des cls & valeurs] cl=jean valeur=20 cl=paul valeur=18 cl=mlanie valeur=10 cl=violette valeur=15 [Suppression d'une cl] [Liste des cls & valeurs] cl=jean valeur=20 cl=mlanie valeur=10 cl=violette valeur=15 Nom recherch (rien pour arrter) : violette violette,15 Nom recherch (rien pour arrter) : x Nom x inconnu

3.5
3.5.1

Les fichiers texte


La classe StreamReader

La classe System.IO.StreamReader permet de lire le contenu d'un fichier texte. Elle est en fait capable d'exploiter des flux qui ne sont pas des fichiers. Voici quelques-unes de ses proprits et mthodes : Constructeurs
public StreamReader(string path) construit un flux de lecture partir du fichier de chemin path. Le contenu du fichier peut tre encod de diverses faons. Il existe un constructeur qui permet de prciser le codage utilis. Par dfaut, c'est le codage UTF-8 qui est utilis.

Proprits
public bool EndOfStream {get;} True si le flux a t lu entirement

Mthodes
public void Close() ferme le flux et libre les ressources alloues pour sa gestion. A faire obligatoirement aprs exploitation du flux. rend le caractre suivant du flux sans le consommer. Un Peek supplmentaire rendrait donc le mme caractre. rend le caractre suivant du flux et avance d'un caractre dans le flux. lit count caractres dans le flux et les met dans buffer partir de la position index. Rend le nombre de caractres lus - peut tre 0. rend la ligne suivante du flux ou null si on tait la fin du flux. rend la fin du flux ou "" si on tait la fin du flux.

public override int Peek()

public override int Read() public override int Read(char[] buffer, int index, int count) public override string ReadLine() public override string ReadToEnd()

Voici un exemple :

Classes .NET d'usage courant

106

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32.

using System; using System.IO; namespace Chap3 { class Program { static void Main(string[] args) { // rpertoire d'excution Console.WriteLine("Rpertoire d'excution : "+Environment.CurrentDirectory); string ligne = null; StreamReader fluxInfos = null; // lecture contenu du fichier infos.txt try { // lecture 1 Console.WriteLine("Lecture 1----------------"); using (fluxInfos = new StreamReader("infos.txt")) { ligne = fluxInfos.ReadLine(); while (ligne != null) { Console.WriteLine(ligne); ligne = fluxInfos.ReadLine(); } } // lecture 2 Console.WriteLine("Lecture 2----------------"); using (fluxInfos = new StreamReader("infos.txt")) { Console.WriteLine(fluxInfos.ReadToEnd()); } } catch (Exception e) { Console.WriteLine("L'erreur suivante s'est produite : " + e.Message); } } } }

ligne 8 : affiche le nom du rpertoire d'excution lignes 12, 27 : un try / catch pour grer une ventuelle exception. ligne 15 : la structure using flux=new StreamReader(...) est une facilit pour ne pas avoir fermer explicitement le flux aprs son exploitation. Cette fermeture est faite automatiquement ds qu'on sort de la porte du using. ligne 15 : le fichier lu s'appelle infos.txt. Comme c'est un nom relatif, il sera cherch dans le rpertoire d'excution affich par la ligne 8. S'il n'y est pas, une exception sera lance et gre par le try / catch. lignes 16-20 : le fichier est lu par lignes successives ligne 25 : le fichier est lu d'un seul coup

Le fichier infos.txt est le suivant :


12620:0:0 13190:0,05:631 15640:0,1:1290,5

et plac dans le dossier suivant du projet C# :

On va dcouvrir que bin/Release est le dossier d'excution lorsque le projet est exccut par Ctrl-F5. L'excution donne les rsultats suivants :
1. 2. Rpertoire d'excution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release Lecture 1----------------

Classes .NET d'usage courant

107

3. 4. 5. 6. 7. 8. 9.

12620:0:0 13190:0,05:631 15640:0,1:1290,5 Lecture 2---------------12620:0:0 13190:0,05:631 15640:0,1:1290,5

Si ligne 15, on met le nom de fichier xx.txt on a les rsultats suivants :


1. 2. 3. Rpertoire d'excution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release Lecture 1---------------L'erreur suivante s'est produite : Could not find file 'C:\...\Chap3\07\bin\Release\xx.txt'.

3.5.2

La classe StreamWriter

La classe System.IO.StreamReader permet d'crire dans un fichier texte. Comme la classe StreamReader, elle est en fait capable d'exploiter des flux qui ne sont pas des fichiers. Voici quelques-unes de ses proprits et mthodes : Constructeurs
public StreamWriter(string path) construit un flux d'criture dans le fichier de chemin path. Le contenu du fichier peut tre encod de diverses faons. Il existe un constructeur qui permet de prciser le codage utilis. Par dfaut, c'est le codage UTF-8 qui est utilis.

Proprits
public virtual bool AutoFlush {get;set;} fixe le mode d'criture dans le fichier du buffer associ au flux. Si gal False, l'criture dans le flux n'est pas immdiate : il y a d'abord criture dans une mmoire tampon puis dans le fichier lorsque la mmoire tampon est pleine sinon l'criture dans le fichier est immdiate (pas de tampon intermdiaire). Par dfaut c'est le mode tamponn qui est utilis. Le tampon n'est crit dans le fichier que lorsqu'il est plein ou bien lorsqu'on le vide explicitement par une opration Flush ou encore lorsqu'on ferme le flux StreamWriter par une opration Close. Le mode AutoFlush=False est le plus efficace lorsqu'on travaille avec des fichiers parce qu'il limite les accs disque. C'est le mode par dfaut pour ce type de flux. Le mode AutoFlush=False ne convient pas tous les flux, notamment les flux rseau. Pour ceux-ci, qui souvent prennent place dans un dialogue entre deux partenaires, ce qui est crit par l'un des partenaires doit tre immdiatement lu par l'autre. Le flux d'criture doit alors tre en mode AutoFlush=True. les caractres de fin de ligne. Par dfaut "\r\n". Pour un systme Unix, il faudrait utiliser "\n".

public virtual string NewLine {get;set;}

Mthodes
public void Close() public override void Flush() public virtual void Write(T value) ferme le flux et libre les ressources alloues pour sa gestion. A faire obligatoirement aprs exploitation du flux. crit dans le fichier, le buffer du flux, sans attendre qu'il soit plein. crit value dans le fichier associ au flux. Ici T n'est pas un type gnrique mais symbolise le fait que la mthode Write accepte diffrents types de paramtres (string, int, object, ...). La mthode value.ToString est utilise pour produire la chane crite dans le fichier. mme chose que Write mais avec la marque de fin de ligne (NewLine) en plus.

public virtual void WriteLine(T value)

Considrons l'exemple suivant :


1. 2. 3. 4. 5. using System; using System.IO; namespace Chap3 { class Program2 {

Classes .NET d'usage courant

108

6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. } 32. }

static void Main(string[] args) { // rpertoire d'excution Console.WriteLine("Rpertoire d'excution : " + Environment.CurrentDirectory); string ligne = null; // une ligne de texte StreamWriter fluxInfos = null; // le fichier texte try { // cration du fichier texte using (fluxInfos = new StreamWriter("infos2.txt")) { Console.WriteLine("Mode AutoFlush : {0}", fluxInfos.AutoFlush); // lecture ligne tape au clavier Console.Write("ligne (rien pour arrter) : "); ligne = Console.ReadLine().Trim(); // boucle tant que la ligne saisie est non vide while (ligne != "") { // criture ligne dans fichier texte fluxInfos.WriteLine(ligne); // lecture nouvelle ligne au clavier Console.Write("ligne (rien pour arrter) : "); ligne = Console.ReadLine().Trim(); }//while } } catch (Exception e) { Console.WriteLine("L'erreur suivante s'est produite : " + e.Message); } }

ligne 13 : de nouveau, nous utilisons la syntaxe using(flux) afin de ne pas avoir fermer explicitement le flux par une opration Close. Cette fermeture est faite automatiquement la sortie du using. pourquoi un try / catch, lignes 11 et 27 ? ligne 13, nous pourrions donner un nom de fichier sous la forme /rep1/rep2/ .../ fichier avec un chemin /rep1/rep2/... qui n'existe pas, rendant ainsi impossible la cration de fichier. Une exception serait alors lance. Il existe d'autres cas d'exception possible (disque plein, droits insuffisants, ...)

Les rsultats d'excution sont les suivants :


1. 2. 3. 4. 5. Rpertoire d'excution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release Mode AutoFlush : False ligne (rien pour arrter) : 1re ligne ligne (rien pour arrter) : 2ime ligne ligne (rien pour arrter) :

Le fichier infos2.txt a t cr dans le dossier bin/Release du projet :

3.6

Les fichiers binaires

Les classes System.IO.BinaryReader et System.IO.BinaryWriter servent lire et crire des fichiers binaires. Considrons l'application suivante :
// syntaxe pg texte bin logs // on lit un fichier texte (texte) et on range son contenu dans un fichier binaire (bin

Classes .NET d'usage courant

109

// le fichier texte a des lignes de la forme nom : age qu'on rangera dans une structure string, int // (logs) est un fichier texte de logs

Le fichier texte a le contenu suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. paul : 10 helene : 15 jacques : 11 sylvain : 12 xx : -1 xx: yy : zz xx : yy

Le programme est le suivant :


1. 2. 3. 4. 5. 6. using System; using System.IO; // syntaxe pg texte bin logs // on lit un fichier texte (texte) et on range son contenu dans un fichier binaire (bin) // le fichier texte a des lignes de la forme nom : age qu'on rangera dans une structure string, int // (logs) est un fichier texte de logs

7. 8. 9. namespace Chap3 { 10. class Program { 11. static void Main(string[] arguments) { 12. // il faut 3 arguments 13. if (arguments.Length != 3) { 14. Console.WriteLine("syntaxe : pg texte binaire log"); 15. Environment.Exit(1); 16. }//if 17. 18. // variables 19. string ligne=null; 20. string nom=null; 21. int age=0; 22. int numLigne = 0; 23. char[] sparateurs = new char[] { ':' }; 24. string[] champs=null; 25. StreamReader input = null; 26. BinaryWriter output = null; 27. StreamWriter logs = null; 28. bool erreur = false; 29. // lecture fichier texte - criture fichier binaire 30. try { 31. // ouverture du fichier texte en lecture 32. input = new StreamReader(arguments[0]); 33. // ouverture du fichier binaire en criture 34. output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write)); 35. // ouverture du fichier des logs en criture 36. logs = new StreamWriter(arguments[2]); 37. // exploitation du fichier texte 38. while ((ligne = input.ReadLine()) != null) { 39. // une ligne de plus 40. numLigne++; 41. // ligne vide ? 42. if (ligne.Trim() == "") { 43. // on ignore 44. continue; 45. } 46. // une ligne nom : age 47. champs = ligne.Split(sparateurs); 48. // il nous faut 2 champs 49. if (champs.Length != 2) { 50. // on logue l'erreur 51. logs.WriteLine("La ligne n [{0}] du fichier [{1}] a un nombre de champs incorrect", numLigne, arguments[0]); 52. // ligne suivante 53. continue; 54. }//if 55. // le 1er champ doit tre non vide 56. erreur = false;

Classes .NET d'usage courant

110

57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86.

nom = champs[0].Trim(); if (nom == "") { // on logue l'erreur logs.WriteLine("La ligne n [{0}] du fichier [{1}] a un nom vide", numLigne, arguments[0]); erreur = true; } // le second champ doit tre un entier >=0 if (!int.TryParse(champs[1],out age) || age<0) { // on logue l'erreur logs.WriteLine("La ligne n [{0}] du fichier [{1}] a un ge [{2}] incorrect", numLigne, arguments[0], champs[1].Trim()); erreur = true; }//if // si pas d'erreur, on crit les donnes dans le fichier binaire if (!erreur) { output.Write(nom); output.Write(age); } // ligne suivante }//while }catch(Exception e){ Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message); } finally { // fermeture des fichiers if(input!=null) input.Close(); if(output!=null) output.Close(); if(logs!=null) logs.Close(); } } } }

Attardons-nous sur les oprations concernant la classe BinaryWriter : ligne 34 : l'objet BinaryWriter est ouvert par l'opration
output=new BinaryWriter(new FileStream(arguments[1],FileMode.Create,FileAccess.Write));

L'argument du constructeur doit tre un flux (Stream). Ici c'est un flux construit partir d'un fichier (FileStream) dont on donne : o le nom o l'opration faire, ici FileMode.Create pour crer le fichier o le type d'accs, ici FileAccess.Write pour un accs en criture au fichier lignes 70-73 : les oprations d'criture
// on crit les donnes dans le fichier binaire output.Write(nom); output.Write(age);

La classe BinaryWriter dispose de diffrentes mthodes Write surcharges pour crire les diffrents types de donnes simples ligne 81 : l'opration de fermeture du flux
output.Close();

Les trois arguments de la mthode Main sont donns au projet (via ses proprits) [1] et le fichier texte exploiter est plac dans le dossier bin/Release [2] :

Classes .NET d'usage courant

111

Avec le fichier [personnes1.txt] suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. paul : 10 helene : 15 jacques : 11 sylvain : 12 xx : -1 xx: yy : zz xx : yy

les rsultats de l'excution sont les suivants :

1. 2. 3.

en [1], le fichier binaire [personnes1.bin] cr ainsi que le fichier de logs [logs.txt]. Celui-ci a le contenu suivant :
La ligne n [6] du fichier [personnes1.txt] a un ge [-1] incorrect La ligne n [8] du fichier [personnes1.txt] a un nombre de champs incorrect La ligne n [9] du fichier [personnes1.txt] a un ge [yy] incorrect

Le contenu du fichier binaire [personnes1.bin] va nous tre donn par le programme qui suit. Celui-ci accepte galement trois arguments :
// // // // // syntaxe pg bin texte logs on lit un fichier binaire bin et on range son contenu dans un fichier texte (texte) le fichier binaire a une structure string, int le fichier texte a des lignes de la forme nom : age logs est un fichier texte de logs

On fait donc l'opration inverse. On lit un fichier binaire pour crer un fichier texte. Si le fichier texte produit est identique au fichier originel on saura que la conversion texte --> binaire --> texte s'est bien passe. Le code est le suivant :
1. 2. 3. 4. 5. 6. using System; using System.IO; // syntaxe pg bin texte logs // on lit un fichier binaire bin et on range son contenu dans un fichier texte (texte) // le fichier binaire a une structure string, int

Classes .NET d'usage courant

112

7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50.

// le fichier texte a des lignes de la forme nom : age // logs est un fichier texte de logs namespace Chap3 { class Program2 { static void Main(string[] arguments) { // il faut 3 arguments if (arguments.Length != 3) { Console.WriteLine("syntaxe : pg binaire texte log"); Environment.Exit(1); }//if

// variables string nom = null; int age = 0; int numPersonne = 1; BinaryReader input = null; StreamWriter output = null; StreamWriter logs = null; bool fini; // lecture fichier binaire - criture fichier texte try { // ouverture du fichier binaire en lecture input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read)); // ouverture du fichier texte en criture output = new StreamWriter(arguments[1]); // ouverture du fichier des logs en criture logs = new StreamWriter(arguments[2]); // exploitation du fichier binaire fini = false; while (!fini) { try { // lecture nom nom = input.ReadString().Trim(); // lecture age age = input.ReadInt32(); // criture dans fichier texte output.WriteLine(nom + ":" + age); // personne suivante numPersonne++; } catch (EndOfStreamException) { fini = true; } catch (Exception e) { logs.WriteLine("L'erreur suivante s'est produite la lecture de la personne n {0} : {1}", numPersonne, e.Message); 51. } 52. }//while 53. } catch (Exception e) { 54. Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message); 55. } finally { 56. // fermeture des fichiers 57. if (input != null) 58. input.Close(); 59. if (output != null) 60. output.Close(); 61. if (logs != null) 62. logs.Close(); 63. } 64. } 65. } 66. }

Attardons-nous sur les oprations concernant la classe BinaryReader : ligne 30 : l'objet BinaryReader est ouvert par l'opration
input=new BinaryReader(new FileStream(arguments[0],FileMode.Open,FileAccess.Read));

L'argument du constructeur doit tre un flux (Stream). Ici c'est un flux construit partir d'un fichier (FileStream) dont on donne : le nom l'opration faire, ici FileMode.Open pour ouvrir un fichier existant le type d'accs, ici FileAccess.Read pour un accs en lecture au fichier

Classes .NET d'usage courant

113

lignes 40, 42 : les oprations de lecture


nom=input.ReadString().Trim(); age=input.ReadInt32();

La classe BinaryReader dispose de diffrentes mthodes ReadXX pour lire les diffrents types de donnes simples ligne 60 : l'opration de fermeture du flux
input.Close();

Si on excute les deux programmes la chane transformant personnes1.txt en personnes1.bin puis personnes1.bin en personnes2.txt2 on obtient les rsultats suivants :

2 3

en [1], le projet est configur pour excuter la 2ime application en [2], les arguments passs Main en [3], les fichiers produits par l'excution de l'application.

Le contenu de [personnes2.txt] est le suivant :


1. 2. 3. 4. paul:10 helene:15 jacques:11 sylvain:12

3.7

Les expressions rgulires

La classe System.Text.RegularExpressions.Regex permet l'utilisation d'expression rgulires. Celles-ci permettent de tester le format d'une chane de caractres. Ainsi on peut vrifier qu'une chane reprsentant une date est bien au format jj/mm/aa. On utilise pour cela un modle et on compare la chane ce modle. Ainsi dans cet exemple, j m et a doivent tre des chiffres. Le modle d'un format de date valide est alors "\d\d/\d\d/\d\d" o le symbole \d dsigne un chiffre. Les symboles utilisables dans un modle sont les suivants :

Classes .NET d'usage courant

114

Caractre
\

^ $ * + ? . (modle)

x|y {n} {n,}

{n,m} [xyz] [^xyz] [a-z] [^m-z] \b \B \d \D \f \n \r \s \S \t \v \w \W \num \n

\xn

Description Marque le caractre suivant comme caractre spcial ou littral. Par exemple, "n" correspond au caractre "n". "\n" correspond un caractre de nouvelle ligne. La squence "\\" correspond "\", tandis que "\(" correspond "(". Correspond au dbut de la saisie. Correspond la fin de la saisie. Correspond au caractre prcdent zro fois ou plusieurs fois. Ainsi, "zo*" correspond "z" ou "zoo". Correspond au caractre prcdent une ou plusieurs fois. Ainsi, "zo+" correspond "zoo", mais pas "z". Correspond au caractre prcdent zro ou une fois. Par exemple, "a?ve?" correspond "ve" dans "lever". Correspond tout caractre unique, sauf le caractre de nouvelle ligne. Recherche le modle et mmorise la correspondance. La sous-chane correspondante peut tre extraite de la collection Matches obtenue, l'aide d'Item [0]...[n]. Pour trouver des correspondances avec des caractres entre parenthses ( ), utilisez "\(" ou "\)". Correspond soit x soit y. Par exemple, "z|foot" correspond "z" ou "foot". "(z|f)oo" correspond "zoo" ou "foo". n est un nombre entier non ngatif. Correspond exactement n fois le caractre. Par exemple, "o{2}" ne correspond pas "o" dans "Bob," mais aux deux premiers "o" dans "fooooot". n est un entier non ngatif. Correspond au moins n fois le caractre. Par exemple, "o{2,}" ne correspond pas "o" dans "Bob", mais tous les "o" dans "fooooot". "o{1,}" quivaut "o+" et "o{0,}" quivaut "o*". m et n sont des entiers non ngatifs. Correspond au moins n et au plus m fois le caractre. Par exemple, "o{1,3}" correspond aux trois premiers "o" dans "foooooot" et "o{0,1}" quivaut "o?". Jeu de caractres. Correspond l'un des caractres indiqus. Par exemple, "[abc]" correspond "a" dans "plat". Jeu de caractres ngatif. Correspond tout caractre non indiqu. Par exemple, "[^abc]" correspond "p" dans "plat". Plage de caractres. Correspond tout caractre dans la srie spcifie. Par exemple, "[a-z]" correspond tout caractre alphabtique minuscule compris entre "a" et "z". Plage de caractres ngative. Correspond tout caractre ne se trouvant pas dans la srie spcifie. Par exemple, "[^m-z]" correspond tout caractre ne se trouvant pas entre "m" et "z". Correspond une limite reprsentant un mot, autrement dit, la position entre un mot et un espace. Par exemple, "er\b" correspond "er" dans "lever", mais pas "er" dans "verbe". Correspond une limite ne reprsentant pas un mot. "en*t\B" correspond "ent" dans "bien entendu". Correspond un caractre reprsentant un chiffre. quivaut [0-9]. Correspond un caractre ne reprsentant pas un chiffre. quivaut [^0-9]. Correspond un caractre de saut de page. Correspond un caractre de nouvelle ligne. Correspond un caractre de retour chariot. Correspond tout espace blanc, y compris l'espace, la tabulation, le saut de page, etc. quivaut "[ \f\n\r\t\v]". Correspond tout caractre d'espace non blanc. quivaut "[^ \f\n\r\t\v]". Correspond un caractre de tabulation. Correspond un caractre de tabulation verticale. Correspond tout caractre reprsentant un mot et incluant un trait de soulignement. quivaut "[AZa-z0-9_]". Correspond tout caractre ne reprsentant pas un mot. quivaut "[^A-Za-z0-9_]". Correspond num, o num est un entier positif. Fait rfrence aux correspondances mmorises. Par exemple, "(.)\1" correspond deux caractres identiques conscutifs. Correspond n, o n est une valeur d'chappement octale. Les valeurs d'chappement octales doivent comprendre 1, 2 ou 3 chiffres. Par exemple, "\11" et "\011" correspondent tous les deux un caractre de tabulation. "\0011" quivaut "\001" & "1". Les valeurs d'chappement octales ne doivent pas excder 256. Si c'tait le cas, seuls les deux premiers chiffres seraient pris en compte dans l'expression. Permet d'utiliser les codes ASCII dans des expressions rgulires. Correspond n, o n est une valeur d'chappement hexadcimale. Les valeurs d'chappement hexadcimales doivent comprendre deux chiffres obligatoirement. Par exemple, "\x41" correspond "A". "\x041" quivaut "\x04" & "1". Permet d'utiliser les codes ASCII dans des expressions 115

Classes .NET d'usage courant

Un lment dans un modle peut tre prsent en 1 ou plusieurs exemplaires. Considrons quelques exemples autour du symbole \d qui reprsente 1 chiffre : modle
\d \d? \d* \d+ \d{2} \d{3,} \d{5,7}

signification un chiffre 0 ou 1 chiffre 0 ou davantage de chiffres 1 ou davantage de chiffres 2 chiffres au moins 3 chiffres entre 5 et 7 chiffres

Imaginons maintenant le modle capable de dcrire le format attendu pour une chane de caractres : chane recherche une date au format jj/mm/aa une heure au format hh:mm:ss un nombre entier non sign un suite d'espaces ventuellement vide un nombre entier non sign qui peut tre prcd ou suivi d'espaces un nombre entier qui peut tre sign et prcd ou suivi d'espaces un nombre rel non sign qui peut tre prcd ou suivi d'espaces un nombre rel qui peut tre sign et prcd ou suivi d'espaces une chane contenant le mot juste On peut prciser o on recherche le modle dans la chane : modle
^modle modle$ ^modle$ modle

modle
\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} \d+ \s* \s*\d+\s* \s*[+|-]?\s*\d+\s* \s*\d+(.\d*)?\s* \s*[+|]?\s*\d+(.\d*)?\s* \bjuste\b

signification le modle commence la chane le modle finit la chane le modle commence et finit la chane le modle est cherch partout dans la chane en commenant par le dbut de celle-ci. modle
!$ \.$ ^// ^\s*\w+\s*$ ^\s*\w+\s*\w+\s*$ \bsecret\b

chane recherche une chane se terminant par un point d'exclamation une chane se terminant par un point une chane commenant par la squence // une chane ne comportant qu'un mot ventuellement suivi ou prcd d'espaces une chane ne comportant deux mot ventuellement suivis ou prcds d'espaces une chane contenant le mot secret

Les sous-ensembles d'un modle peuvent tre "rcuprs". Ainsi non seulement, on peut vrifier qu'une chane correspond un modle particulier mais on peut rcuprer dans cette chane les lments correspondant aux sous-ensembles du modle qui ont t entours de parenthses. Ainsi si on analyse une chane contenant une date jj/mm/aa et si on veut de plus rcuprer les lments jj, mm, aa de cette date on utilisera le modle (\d\d)/(\d\d)/(\d\d).

3.7.1

Vrifier qu'une chane correspond un modle donn

Un objet de type Regex se construit de la faon suivante :


public Regex(string pattern) construit un objet "expression rgulire" partir d'un modle pass en paramtre (pattern)

Une fois l'expression rgulire modle construit, on peut la comparer des chanes de caractres avec la mthode IsMatch :
public bool IsMatch(string input) vrai si la chane input correspond au modle de l'expression rgulire

Classes .NET d'usage courant

116

Voici un exemple :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. using System; using System.Text.RegularExpressions; namespace Chap3 { class Program { static void Main(string[] args) { // une expression rgulire modle string modle1 = @"^\s*\d+\s*$"; Regex regex1 = new Regex(modle1); // comparer un exemplaire au modle string exemplaire1 = " 123 "; if (regex1.IsMatch(exemplaire1)) { Console.WriteLine("[{0}] correspond au } else { Console.WriteLine("[{0}] ne correspond }//if string exemplaire2 = " 123a "; if (regex1.IsMatch(exemplaire2)) { Console.WriteLine("[{0}] correspond au } else { Console.WriteLine("[{0}] ne correspond }//if } } }

modle [{1}]", exemplaire1, modle1); pas au modle [{1}]", exemplaire1, modle1);

modle [{1}]", exemplaire2, modle1); pas au modle [{1}]", exemplaire2, modle1);

et les rsultats d'excution :


1. 2. [ [ 123 ] correspond au modle [^\s*\d+\s*$] 123a ] ne correspond pas au modle [^\s*\d+\s*$]

3.7.2

Trouver toutes les occurrences d'un modle dans une chane

La mthode Matches permet de rcuprer les lments d'une chane correspondant un modle :
public MatchCollection Matches(string input) rend la collection des lments de la chane input correspondant au modle

La classe MatchCollection a une proprit Count qui est le nombre d'lments de la collection. Si rsultats est un objet MatchCollection, rsultats[i] est l'lment i de cette collection et est de type Match. La classe Match a diverses proprits dont les suivantes : Value : la valeur de l'objet Match, donc un lment correspondant au modle Index : la position o l'lment a t trouv dans la chane explore Examinons l'exemple suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System; using System.Text.RegularExpressions; namespace Chap3 { class Program2 { static void Main(string[] args) { // plusieurs occurrences du modle dans l'exemplaire string modle2 = @"\d+"; Regex regex2 = new Regex(modle2); string exemplaire3 = " 123 456 789 "; MatchCollection rsultats = regex2.Matches(exemplaire3); Console.WriteLine("Modle=[{0}],exemplaire=[{1}]", modle2, exemplaire3); Console.WriteLine("Il y a {0} occurrences du modle dans l'exemplaire ", rsultats.Count); for (int i = 0; i < rsultats.Count; i++) { Console.WriteLine("[{0}] trouv en position {1}", rsultats[i].Value, rsultats[i].Index); }//for } } }

Classes .NET d'usage courant

117

ligne 8 : le modle recherch est une suite de chiffres ligne 10 : la chane dans laquelle on recherche ce modle ligne 11 : on rcupre tous les lments de exemplaire3 vrifiant le modle modle2 lignes 14-16 : on les affiche

Les rsultats de l'excution du programme sont les suivants :


1. 2. 3. 4. 5. Modle=[\d+],exemplaire=[ 123 456 789 ] Il y a 3 occurrences du modle dans l'exemplaire [123] trouv en position 2 [456] trouv en position 7 [789] trouv en position 12

3.7.3

Rcuprer des parties d'un modle

Des sous-ensembles d'un modle peuvent tre "rcuprs". Ainsi non seulement, on peut vrifier qu'une chane correspond un modle particulier mais on peut rcuprer dans cette chane les lments correspondant aux sous-ensembles du modle qui ont t entours de parenthses. Ainsi si on analyse une chane contenant une date jj/mm/aa et si on veut de plus rcuprer les lments jj, mm, aa de cette date on utilisera le modle (\d\d)/(\d\d)/(\d\d). Examinons l'exemple suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. using System; using System.Text.RegularExpressions; namespace Chap3 { class Program3 { static void Main(string[] args) { // capture d'lments dans le modle string modle3 = @"(\d\d):(\d\d):(\d\d)"; Regex regex3 = new Regex(modle3); string exemplaire4 = "Il est 18:05:49"; // vrification modle Match rsultat = regex3.Match(exemplaire4); if (rsultat.Success) { // l'exemplaire correspond au modle Console.WriteLine("L'exemplaire [{0}] correspond au modle [{1}]",exemplaire4,modle3); // on affiche les groupes de parenthses for (int i = 0; i < rsultat.Groups.Count; i++) { Console.WriteLine("groupes[{0}]=[{1}] trouv en position {2}",i, rsultat.Groups[i].Value,rsultat.Groups[i].Index); }//for } else { // l'exemplaire ne correspond pas au modle Console.WriteLine("L'exemplaire[{0}] ne correspond pas au modle [{1}]", exemplaire4, modle3); } } } }

L'excution de ce programme produit les rsultats suivants :


1. 2. 3. 4. 5. L'exemplaire [Il est 18:05:49] correspond au modle [(\d\d):(\d\d):(\d\d)] groupes[0]=[18:05:49] trouv en position 7 groupes[1]=[18] trouv en position 7 groupes[2]=[05] trouv en position 10 groupes[3]=[49] trouv en position 13

La nouveaut se trouve dans les lignes 12-19 :

ligne 12 : la chane exemplaire4 est compare au modle regex3 au travers de la mthode Match. Celle-ci rend un objet Match dj prsent. Nous utilisons ici deux nouvelles proprits de cette classe : Success (ligne 13) : indique s'il y a eu correspondance Groups (lignes 17, 18) : collection o o Groups[0] correspond la partie de la chane correspondant au modle o Groups[i] (i>=1) correspond au groupe de parenthses n i

Classes .NET d'usage courant

118

Si rsultat est de type Match, rsultats.Groups est de type GroupCollection et rsultats.Groups[i] de type Group. La classe Group a deux proprits que nous utilisons ici : Value (ligne 18) : la valeur de l'objet Group qui est l'lment correspondant au contenu d'une parenthse Index (ligne 18) : la position o l'lment a t trouv dans la chane explore

3.7.4

Un programme d'apprentissage

Trouver l'expression rgulire qui permet de vrifier qu'une chane correspond bien un certain modle est parfois un vritable dfi. Le programme suivant permet de s'entraner. Il demande un modle et une chane et indique si la chane correspond ou non au modle.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. using System; using System.Text.RegularExpressions; namespace Chap3 { class Program4 { static void Main(string[] args) { // donnes string modle, chaine; Regex regex = null; MatchCollection rsultats; // on demande l'utilisateur les modles et les exemplaires comparer celui-ci while (true) { // on demande le modle Console.Write("Tapez le modle tester ou rien pour arrter :"); modle = Console.In.ReadLine(); // fini ? if (modle.Trim() == "") break; // on cre l'expression rgulire try { regex = new Regex(modle); } catch (Exception ex) { Console.WriteLine("Erreur : " + ex.Message); continue; } // on demande l'utilisateur les exemplaires comparer au modle while (true) { Console.Write("Tapez la chane comparer au modle [{0}] ou rien pour arrter :", modle); chaine = Console.ReadLine(); // fini ? if (chaine.Trim() == "") break; // on fait la comparaison rsultats = regex.Matches(chaine); // succs ? if (rsultats.Count == 0) { Console.WriteLine("Je n'ai pas trouv de correspondances"); continue; }//if // on affiche les lments correspondant au modle for (int i = 0; i < rsultats.Count; i++) { Console.WriteLine("J'ai trouv la correspondance [{0}] en position [{1}]", rsultats[i].Value, rsultats[i].Index); // des sous-lments if (rsultats[i].Groups.Count != 1) { for (int j = 1; j < rsultats[i].Groups.Count; j++) { Console.WriteLine("\tsous-lment [{0}] en position [{1}]", rsultats[i].Groups[j].Value, rsultats[i].Groups[j].Index); } } } } } } } }

Voici un exemple d'excution :


1. Tapez le modle tester ou rien pour arrter :\d+

Classes .NET d'usage courant

119

2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27.

Tapez la chane comparer au modle [\d+] ou rien pour arrter :123 456 789 J'ai trouv la correspondance [123] en position [0] J'ai trouv la correspondance [456] en position [4] J'ai trouv la correspondance [789] en position [8] Tapez la chane comparer au modle [\d+] ou rien pour arrter : Tapez le modle tester ou rien pour arrter :(\d{2}):(\d\d) Tapez la chane comparer au modle [(\d{2}):(\d\d)] ou rien pour arrter :14:15 abcd 17:18 xyzt J'ai trouv la correspondance [14:15] en position [0] sous-lment [14] en position [0] sous-lment [15] en position [3] J'ai trouv la correspondance [17:18] en position [11] sous-lment [17] en position [11] sous-lment [18] en position [14] Tapez la chane comparer au modle [(\d{2}):(\d\d)] ou rien pour arrter : Tapez le modle tester ou rien pour arrter :^\s*\d+\s*$ Tapez la chane comparer au modle [^\s*\d+\s*$] ou rien pour arrter : 1456 J'ai trouv la correspondance [ 1456] en position [0] Tapez la chane comparer au modle [^\s*\d+\s*$] ou rien pour arrter : Tapez le modle tester ou rien pour arrter :^\s*(\d+)\s*$ Tapez la chane comparer au modle [^\s*(\d+)\s*$] ou rien pour arrter :1456 J'ai trouv la correspondance [1456] en position [0] sous-lment [1456] en position [0] Tapez la chane comparer au modle [^\s*(\d+)\s*$] ou rien pour arrter :abcd 1456 Je n'ai pas trouv de correspondances Tapez la chane comparer au modle [^\s*(\d+)\s*$] ou rien pour arrter : Tapez le modle tester ou rien pour arrter :

3.7.5

La mthode Split

Nous avons dj rencontr cette mthode dans la classe String :


public string[] Split(char[] separator) la chane est vue comme une suite de champs spars par les caractres prsents dans le tableau separator. Le rsultat est le tableau de ces champs

La mthode Split de la classe Regex nous permet d'exprimer le sparateur en fonction d'un modle :
public string[] Split(string input) La chane input est dcompose en champs, ceux-ci tant spars par un sparateur correspondant au modle de l'objet Regex courant.

Supposons par exemple qu'on ait dans un fichier texte des lignes de la forme champ1, champ2, .., champn. Les champs sont spars par une virgule mais celle-ci peut tre prcde ou suivie d'espaces. La mthode Split de la classe string ne convient alors pas. Celle de la mthode RegEx apporte la solution. Si ligne est la ligne lue, les champs pourront tre obtenus par
string[] champs=new Regex(@"s*,\s*").Split(ligne);

comme le montre l'exemple suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System; using System.Text.RegularExpressions; namespace Chap3 { class Program5 { static void Main(string[] args) { // une ligne string ligne = "abc , def , ghi"; // un modle Regex modle = new Regex(@"\s*,\s*"); // dcomposition de ligne en champs string[] champs = modle.Split(ligne); // affichage for (int i = 0; i < champs.Length; i++) { Console.WriteLine("champs[{0}]=[{1}]", i, champs[i]); } } } }

Les rsultats d'excution :

Classes .NET d'usage courant

120

1. 2. 3.

champs[0]=[abc] champs[1]=[def] champs[2]=[ghi]

3.8

Application exemple - V3

Nous reprenons l'application tudie aux paragraphes 1.6 page 31 (version 1) et 2.10 page 84 (version 2). Dans la dernire version tudie, le calcul de l'impt se faisait dans la classe abstraite AbstractImpot :
29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. namespace Chap2 { abstract class AbstractImpot : IImpot { // les tranches d'impt ncessaires au calcul de l'impt // proviennent d'une source extrieure protected TrancheImpot[] tranchesImpot; // calcul de l'impt public int calculer(bool mari, int nbEnfants, int salaire) { // calcul du nombre de parts decimal nbParts; if (mari) nbParts = (decimal)nbEnfants / 2 + 2; else nbParts = (decimal)nbEnfants / 2 + 1; if (nbEnfants >= 3) nbParts += 0.5M; // calcul revenu imposable & Quotient familial decimal revenu = 0.72M * salaire; decimal QF = revenu / nbParts; // calcul de l'impt tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1; int i = 0; while (QF > tranchesImpot[i].Limite) i++; // retour rsultat return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN); }//calculer }//classe }

La mthode calculer de la ligne 38 utilise le tableau tranchesImpot de la ligne 35, tableau non initialis par la classe AbstractImpot. C'est pourquoi elle est abstraite et doit tre drive pour tre utile. Cette initialisation tait faite par la classe drive HardwiredImpot :
21. using System; 22. 23. namespace Chap2 { 24. class HardwiredImpot : AbstractImpot { 25. 26. // tableaux de donnes ncessaires au calcul de l'impt 27. decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M }; 28. decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M }; 29. decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M }; 30. 31. public HardwiredImpot() { 32. // cration du tableau des tranches d'impt 33. tranchesImpot = new TrancheImpot[limites.Length]; 34. // remplissage 35. for (int i = 0; i < tranchesImpot.Length; i++) { 36. tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] }; 37. } 38. } 39. }// classe 40. }// namespace

Ci-dessus, les donnes ncessaires au calcul de l'impt taient places en "dur" dans le code de la classe. La nouvelle version de l'exemple les place dans un fichier texte :
4962:0:0 8382:0,068:291,09 14753:0,191:1322,92

Classes .NET d'usage courant

121

23888:0,283:2668,39 38868:0,374:4846,98 47932:0,426:6883,66 0:0,481:9505,54

L'exploitation de ce fichier pouvant produire des exceptions, nous crons une classe spciale pour grer ces dernires :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. using System; namespace Chap3 { class FileImpotException : Exception { // codes d'erreur [Flags] public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 }; // code d'erreur public CodeErreurs Code { get; set; } // constructeurs public FileImpotException() { } public FileImpotException(string message) : base(message) { } public FileImpotException(string message, Exception e) : base(message,e) { } } }

ligne 4 : la classe FileImpotException drive de la classe Exception. Elle servira mmoriser toute erreur survenant lors de l'exploitation du fichier texte des donnes. ligne 7 : une numration reprsentant des codes d'erreur : Acces : erreur d'accs au fichier texte des donnes Ligne : ligne n'ayant pas les trois champs attendus Champ1 : le champ n 1 est erron Champ2 : le champ n 2 est erron Champ3 : le champ n 3 est erron Certaines de ces erreurs peuvent se combiner (Champ1, Champ2, Champ3). Aussi l'numration CodeErreurs a-t-elle t annote avec l'attribut [Flags] qui implique que les diffrentes valeurs de l'numration doivent tre des puissances de 2. Une erreur sur les champs 1 et 2 se traduira alors par le code d'erreur Champ1 | Champ2. ligne 10 : la proprit automatique Code mmorisera le code de l'erreur. lignes 15 : un constructeur permettant de construire un objet FileImpotException en lui passant comme paramtre un message d'erreur. lignes 18 : un constructeur permettant de construire un objet FileImpotException en lui passant comme paramtres un message d'erreur et l'exception l'origine de l'erreur.

La classe qui initialise le tableau tranchesImpot de la classe AbstractImpot est dsormais la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. using using using using System; System.Collections.Generic; System.IO; System.Text.RegularExpressions;

namespace Chap3 { class FileImpot : AbstractImpot { public FileImpot(string fileName) { // donnes List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>(); int numLigne = 1; // exception FileImpotException fe = null; // lecture contenu du fichier fileName, ligne par ligne Regex pattern = new Regex(@"s*:\s*"); // au dpart pas d'erreur FileImpotException.CodeErreurs code = 0; try { using (StreamReader input = new StreamReader(fileName)) { while (!input.EndOfStream && code == 0) { // ligne courante string ligne = input.ReadLine().Trim();

Classes .NET d'usage courant

122

24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64.

// on ignore les lignes vides if (ligne == "") continue; // ligne dcompose en trois champs spars par : string[] champsLigne = pattern.Split(ligne); // a-t-on 3 champs ? if (champsLigne.Length != 3) { code = FileImpotException.CodeErreurs.Ligne; } // conversions des 3 champs decimal limite = 0, coeffR = 0, coeffN = 0; if (code == 0) { if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1; if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2; if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ; } // erreur ? if (code != 0) { // on note l'erreur fe = new FileImpotException(String.Format("Ligne n {0} incorrecte", numLigne)) { Code = code }; } else { // on mmorise la nouvelle tranche d'impt listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN }); // ligne suivante numLigne++; } } } // on transfre la liste listImpot dans le tableau tranchesImpot if (code == 0) { // on transfre la liste listImpot dans le tableau tranchesImpot tranchesImpot = listTranchesImpot.ToArray(); } } catch (Exception e) { // on note l'erreur fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces }; } // erreur signaler ? if (fe != null) throw fe; } } }

ligne 7 : la classe FileImpot drive de la classe AbstractImpot comme le faisait dans la version 2 la classe HardwiredImpot. ligne 9 : le constructeur de la classe FileImpot a pour rle d'initialiser le champ trancheImpot de sa classe de base AbstractImpot. Il admet pour paramtre, le nom du fichier texte contenant les donnes. ligne 11 : le champ tranchesImpot de la classe de base AbstractImpot est un tableau qui a tre rempli avec les donnes du fichier filename pass en paramtre. La lecture d'un fichier texte est squentielle. On ne connat le nombre de lignes qu'aprs avoir lu la totalit du fichier. Aussi ne peut-on dimensionner le tableau tranchesImpot. On mmorisera momentanment les donnes dans la liste gnrique listTranchesImpot. On rappelle que le type TrancheImpot est une structure :
namespace Chap3 { // une tranche d'impt struct TrancheImpot { public decimal Limite { get; set; } public decimal CoeffR { get; set; } public decimal CoeffN { get; set; } } }

ligne 14 : fe de type FileImpotException sert encapsuler une ventuelle erreur d'exploitation du fichier texte. ligne 16 : l'expression rgulire du sparateur de champs dans une ligne champ1:champ2:champ3 du fichier texte. Les champs sont spars par le caractre : prcd et suivi d'un nombre quelconque d'espaces. ligne 18 : le code de l'erreur en cas d'erreur ligne 20 : exploitation du fichier texte avec un StreamReader ligne 21 : on boucle tant qu'il reste une ligne lire et qu'il n'y a pas eu d'erreur

Classes .NET d'usage courant

123

ligne 27 : la ligne lue est divise en champs grce l'expression rgulire de la ligne 16 lignes 29-31 : on vrifie que la ligne a bien trois champs - on note une ventuelle erreur lignes 33-38 : conversion des trois chanes en trois nombres dcimaux - on note les ventuelles erreurs lignes 40-43 : s'il y a eu erreur, une exception de type FileImpotException est cre. lignes 44-47 : s'il n'y a pas eu d'erreur, on passe la lecture de la ligne suivante du fichier texte aprs avoir mmoris les donnes issues de la ligne courante. lignes 52-55 : la sortie de la bouche while, les donnes de la liste gnrique listTranchesImpot sont recopies dans le tableau tranchesImpot de la classe de base AbstractImpot. On rappelle que tel tait le but du constructeur. lignes 56-59 : gestion d'une ventuelle exception. Celle-ci est encapsule dans un objet de type FileImpotException. ligne 61 : si l'exception fe de la ligne 18 a t initialise, alors elle est lance.

L'ensemble du projet C# est le suivant : 4 3

2 1

en [1] : l'ensemble du projet en [2,3] : les proprits du fichier [DataImpot.txt] [2]. La proprit [Copy to Output Directory] [3] est mise always. Ceci fait que le fichier [DataImpot.txt] sera copi dans le dossier bin/Release (mode Release) ou bin/Debug (mode Debug) chaque excution. C'est l qu'il est cherch par l'excutable. en [4] : on fait de mme avec le fichier [DataImpotInvalide.txt].

Le contenu de [DataImpot.txt] est le suivant :


4962:0:0 8382:0,068:291,09 14753:0,191:1322,92 23888:0,283:2668,39 38868:0,374:4846,98 47932:0,426:6883,66 0:0,481:9505,54

Le contenu de [DataImpotInvalide.txt] est le suivant :


a:b:c

Le programme de test [Program.cs] n'a pas chang : c'est celui de la version 2 page 86, la diffrence prs suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. using System;

namespace Chap3 { class Program { static void Main() { ... // cration d'un objet IImpot IImpot impot = null; try { // cration d'un objet IImpot impot = new FileImpot("DataImpot.txt"); } catch (FileImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); 15. Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); 16. // arrt programme 17. Environment.Exit(1);

Classes .NET d'usage courant

124

18. 19. 20. 21. 22. ... 23. 24. } 25. } 26. }

} // boucle infinie while (true) { }//while

ligne 8 : objet impot du type de l'interface IImpot ligne 11 : instanciation de l'objet impot avec un objet de type FileImpot. Celle-ci peut gnrer une exception qui est gre par le try / catch des lignes 9 / 12 / 18.

Voici des exemples d'excution : Avec le fichier [DataImpot.txt]


1. 2. 3. Paramtres du calcul de l'Impot au format : Mari (o/n) NbEnfants Salaire ou rien pour arrter :o 2 60000 Impot=4282 euros Paramtres du calcul de l'Impot au format : Mari (o/n) NbEnfants Salaire ou rien pour arrter :

Avec un fichier [xx] inexistant


1. L'erreur suivante s'est produite : [Code=Acces,Message=Erreur lors de la lecture du fichier xx, Exception d'origine : Could not find file 'C:\data\2007-2008\c#2008\poly\Chap3\10\bin\Release\xx'.]

Avec le fichier [DataImpotInvalide.txt]


1. L'erreur suivante s'est produite : [Code=Champ1, Champ2, Champ3,Message=Ligne n 1 incorrecte]

Classes .NET d'usage courant

125

4
4.1

Architectures 3 couches, tests unitaires NUnit, framework Spring


Introduction

Revenons sur la dernire version de l'application de calcul d'impt :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. using System; namespace Chap3 { class Program { static void Main() { // programme interactif de calcul d'Impot // l'utilisateur tape trois donnes au clavier : mari nbEnfants salaire // le programme affiche alors l'Impot payer ... // cration d'un objet IImpot IImpot impot = null; try { // cration d'un objet IImpot impot = new FileImpot("DataImpotInvalide.txt"); } catch (FileImpotException e) { // affichage erreur ... // arrt programme Environment.Exit(1);

} // boucle infinie while (true) { // on demande les paramtres du calcul de l'impt Console.Write("Paramtres du calcul de l'Impot au format : Mari (o/n) NbEnfants Salaire ou rien pour arrter :"); 26. string paramtres = Console.ReadLine().Trim(); 27. ... 28. // les paramtres sont corrects - on calcule l'Impot 29. Console.WriteLine("Impot=" + impot.calculer(mari == "o", nbEnfants, salaire) + " euros"); 30. // contribuable suivant 31. }//while 32. } 33. } 34. }

La solution prcdente inclut des traitements classiques en programmation : 1. 2. 3. la rcupration de donnes mmorises dans des fichiers, bases de donnes, ... lignes 12-21 le dialogue avec l'utilisateur, lignes 26 (saisies) et 29 (affichages) l'utilisation d'un algorithme mtier, ligne 29

La pratique a montr qu'isoler ces diffrents traitements dans des classes spares amliorait la maintenabilit des applications. L'architecture d'une application ainsi structure est la suivante :

utilisateur

Couche ui [ui]

Couche mtier [metier]

Couche d'accs aux donnes [dao]

Donnes

SPRING On appelle cette architecture, "architecture trois tiers", traduction de l'anglais "three tier architecture". Le terme "trois tiers" dsigne normalement une architecture o chaque tier est sur une machine diffrente. Lorsque les tiers sont sur une mme machine, l'architecture devient une architecture "trois couches".

Architectures 3 couches, tests unitaires NUnit, framework Spring

126

la couche [metier] est celle qui contient les rgles mtier de l'application. Pour notre application de calcul d'impt, ce sont les rgles qui permettent de calculer l'impt d'un contribuable. Cette couche a besoin de donnes pour travailler : les tranches d'impt, donnes qui changent chaque anne le nombre d'enfants, le statut marital et le salaire annuel du contribuable Dans le schma ci-dessus, les donnes peuvent provenir de deux endroits :

la couche d'accs aux donnes ou [dao] (DAO = Data Access Object) pour les donnes dj enregistres dans des fichiers ou bases de donnes. Ce pourrait tre le cas ici des tranches d'impt comme il a t fait dans la version prcdente de l'application. la couche d'interface avec l'utilisateur ou [ui] (UI = User Interface) pour les donnes saisies par l'utilisateur ou affiches l'utilisateur. Ce pourrait tre le cas ici du nombre d'enfants, du statut marital et du salaire annuel du contribuable

de faon gnrale, la couche [dao] s'occupe de l'accs aux donnes persistantes (fichiers, bases de donnes) ou non persistantes (rseau, capteurs, ...). la couche [ui] elle, s'occupe des interactions avec l'utilisateur s'il y en a un. les trois couches sont rendues indpendantes grce l'utilisation d'interfaces.

Nous allons reprendre l'application [Impots] dj tudie plusieurs reprises pour lui donner une architecture 3 couches. Pour cela, nous allons tudier les couches [ui, metier, dao] les unes aprs les autres, en commenant par la couche [dao], couche qui s'occupe des donnes persistantes. Auparavant, il nous faut dfinir les interfaces des diffrentes couches de l'application [Impots].

4.2

Les interfaces de l'application [Impots]

Rappelons qu'une interface dfinit un ensemble de signatures de mthodes. Les classes implmentant l'interface donnent un contenu ces mthodes. Revenons l'architecture 3 couches de notre application :

1
utilisateur

Couche ui [ui]

2 7

Couche mtier [metier]

4 3 Couche d'accs aux


donnes [dao]

Donnes

6
SPRING

Dans ce type d'architecture, c'est souvent l'utilisateur qui prend les initiatives. Il fait une demande en [1] et reoit une rponse en [8]. On appelle cela le cycle demande - rponse. Prenons l'exemple du calcul de l'impt d'un contribuable. Celui-ci va ncessiter plusieurs tapes : (a) la couche [ui] va devoir demander l'utilisateur son nombre d'enfants, son statut marital et son salaire annuel. C'est l'opration [1] ci-dessus. (b) ceci fait, la couche [ui] va demander la couche mtier de faire le calcul de l'impt. Pour cela elle va lui transmettre les donnes qu'elle a reues de l'utilisateur. C'est l'opration [2]. (c) la couche [metier] a besoin de certaines informations pour mener bien son travail : les tranches d'impt. Elle va demander ces informations la couche [dao] avec le chemin [3, 4, 5, 6]. [3] est la demande initiale et [6] la rponse cette demande. (d) ayant toutes les donnes dont elle avait besoin, la couche [metier] calcule l'impt. (e) la couche [metier] peut maintenant rpondre la demande de la couche [ui] faite en (b). C'est le chemin [7]. (f) la couche [ui] va mettre en forme ces rsultats puis les prsenter l'utilisateur. C'est le chemin [8]. (g) on pourrait imaginer que l'utilisateur fait des simulations d'impt et qu'il veuille mmoriser celles-ci. Il utilisera le chemin [1-8] pour le faire. On voit dans cette description qu'une couche est amene utiliser les ressources de la couche qui est sa droite, jamais de celle qui est sa gauche. Considrons deux couches contiges :

Architectures 3 couches, tests unitaires NUnit, framework Spring

127

Couche [A]

1 2

Couche [B]

La couche [A] fait des demandes la couche [B]. Dans les cas les plus simples, une couche est implmente par une unique classe. Une application volue au cours du temps. Ainsi la couche [B] peut avoir des classes d'implmentation diffrentes [B1, B2, ...]. Si la couche [B] est la couche [dao], celle-ci peut avoir une premire implmentation [B1] qui va chercher des donnes dans un fichier. Quelques annes plus tard, on peut vouloir mettre les donnes dans une base de donnes. On va alors construire une seconde classe d'implmentation [B2]. Si dans l'application initiale, la couche [A] travaillait directement avec la classe [B1] on est obligs de rcrire partiellement le code de la couche [A]. Supposons par exemple qu'on ait crit dans la couche [A] quelque chose comme suit :
1. B1 b1=new B1(...); 2. .. 3. b1.getData(...);

ligne 1 : une instance de la classe [B1] est cre ligne 3 : des donnes sont demandes cette instance

Si on suppose, que la nouvelle classe d'implmentation [B2] utilise des mthodes de mme signature que celle de la classe [B1], il faudra changer tous les [B1] en [B2]. Ca, c'est le cas trs favorable et assez improbable si on n'a pas prt attention ces signatures de mthodes. Dans la pratique, il est frquent que les classes [B1] et [B2] n'aient pas les mmes signatures de mthodes et que donc une bonne partie de la couche [A] doive tre totalement rcrite. On peut amliorer les choses si on met une interface entre les couches [A] et [B]. Cela signifie qu'on fige dans une interface les signatures des mthodes prsentes par la couche [B] la couche [A]. Le schma prcdent devient alors le suivant :

Couche [A]

1 4

Interface IB

2 3

Couche [B]

La couche [A] ne s'adresse dsormais plus directement la couche [B] mais son interface [IB]. Ainsi dans le code de la couche [A], la classe d'implmentation [Bi] de la couche [B] n'apparat qu'une fois, au moment de l'implmentation de l'interface [IB]. Ceci fait, c'est l'interface [IB] et non sa classe d'implmentation qui est utilise dans le code. Le code prcdent devient celui-ci :
1. IB ib=new B1(...); 2. .. 3. ib.getData(...);

ligne 1 : une instance [ib] implmentant l'interface [IB] est cre par instanciation de la classe [B1] ligne 3 : des donnes sont demandes l'instance [ib]

Dsormais si on remplace l'implmentation [B1] de la couche [B] par une implmentation [B2], et que ces deux implmentations respectent la mme interface [IB], alors seule la ligne 1 de la couche [A] doit tre modifie et aucune autre. C'est un grand avantage qui lui seul justifie l'usage systmatique des interfaces entre deux couches. On peut aller encore plus loin et rendre la couche [A] totalement indpendante de la couche [B]. Dans le code ci-dessus, la ligne 1 pose problme parce qu'elle rfrence en dur la classe [B1]. L'idal serait que la couche [A] puisse disposer d'une implmentation de l'interface [IB] sans avoir nommer de classe. Ce serait cohrent avec notre schma ci-dessus. On y voit que la couche [A] s'adresse l'interface [IB] et on ne voit pas pourquoi elle aurait besoin de connatre le nom de la classe qui implmente cette interface. Ce dtail n'est pas utile la couche [A]. Le framework Spring (http://www.springframework.org) permet d'obtenir ce rsultat. L'architecture prcdente volue de la faon suivante :

Architectures 3 couches, tests unitaires NUnit, framework Spring

128

Couche [A]

1 4

Interface IB

2 3

Couche [B]

SPRING
La couche transversale [Spring] va permettre une couche d'obtenir par configuration une rfrence sur la couche situe sa droite sans avoir connatre le nom de la classe d'implmentation de la couche. Ce nom sera dans les fichiers de configuration et non dans le code C#. Le code C# de la couche [A] prend alors la forme suivante :
1. 2. 3. IB ib; // initialis par Spring .. ib.getData(...);

ligne 1 : une instance [ib] implmentant l'interface [IB] de la couche [B]. Cette instance est cre par Spring sur la base d'informations trouves dans un fichier de configuration. Spring va s'occuper de crer : l'instance [b] implmentant la couche [B] l'instance [a] implmentant la couche [A]. Cette instance sera initialise. Le champ [ib] ci-dessus recevra pour valeur la rfrence [b] de l'objet implmentant la couche [B] ligne 3 : des donnes sont demandes l'instance [ib]

On voit maintenant que, la classe d'implmentation [B1] de la couche B n'apparat nulle part dans le code de la couche [A]. Lorsque l'implmentation [B1] sera remplace par une nouvelle implmentation [B2], rien ne changera dans le code de la classe [A]. On changera simplement les fichiers de configuration de Spring pour instancier [B2] au lieu de [B1]. Le couple Spring et interfaces C# apporte une amlioration dcisive la maintenance d'applications en rendant les couches de celles-ci tanches entre elles. C'est cette solution que nous utiliserons pour une nouvelle version de l'application [Impots]. Revenons l'architecture trois couches de notre application :

1
utilisateur

Couche ui [ui]

2 7

Couche mtier [metier]

4 3 Couche d'accs aux


donnes [dao]

Donnes

6
SPRING

Dans les cas simples, on peut partir de la couche [metier] pour dcouvrir les interfaces de l'application. Pour travailler, elle a besoin de donnes :

dj disponibles dans des fichiers, bases de donnes ou via le rseau. Elles sont fournies par la couche [dao]. pas encore disponibles. Elles sont alors fournies par la couche [ui] qui les obtient auprs de l'utilisateur de l'application.

Quelle interface doit offrir la couche [dao] la couche [metier] ? Quelles sont les interactions possibles entre ces deux couches ? La couche [dao] doit fournir les donnes suivantes la couche [metier] :

les tranches d'impt

Dans notre application, la couche [dao] exploite des donnes existantes mais n'en cre pas de nouvelles. Une dfinition de l'interface de la couche [dao] pourrait tre la suivante :
1. 2. 3. 4. 5. 6. 7. 8. using Entites; namespace Dao { public interface IImpotDao { // les tranches d'impt TrancheImpot[] TranchesImpot{get;} } }

Architectures 3 couches, tests unitaires NUnit, framework Spring

129


1. 2. 3. 4. 5. 6. 7. 8.

ligne 3 : la couche [dao] sera place dans l'espace de noms [Dao] ligne 6 : l'interface IImpotDao dfinit la proprit TranchesImpot qui fournira les tranches d'impt la couche [mtier]. ligne 1 : importe l'espace de noms dans lequel est dfinie la structure TrancheImpot :
namespace Entites { // une tranche d'impt public struct TrancheImpot public decimal Limite { public decimal CoeffR { public decimal CoeffN { } }

{ get; set; } get; set; } get; set; }

Revenons l'architecture trois couches de notre application :

1
utilisateur

Couche ui [ui]

2 7

Couche mtier [metier]

4 3 Couche d'accs aux


donnes [dao]

Donnes

6
SPRING

Quelle interface la couche [metier] doit-elle prsenter la couche [ui] ? Rappelons les interactions entre ces deux couches : (a) la couche [ui] demandee l'utilisateur son nombre d'enfants, son statut marital et son salaire annuel. C'est l'opration [1] ci-dessus. (b) ceci fait, la couche [ui] va demander la couche mtier de faire le calcul des siges. Pour cela elle va lui transmettre les donnes qu'elle a reues de l'utilisateur. C'est l'opration [2]. Une dfinition de l'interface de la couche [metier] pourrait tre la suivante :
1. 2. 3. 4. 5. namespace Metier { interface IImpotMetier { int CalculerImpot(bool mari, int nbEnfants, int salaire); } }

ligne 1 : on mettra tout ce qui concerne la couche [metier] dans l'espace de noms [Metier]. ligne 2 : l'interface IImpotMetier ne dfinit qu'une mthode : celles qui permet de calculer l'impt d'un contribuable partir de son tat marital, son nombre d'enfants et son salaire annuel.

Nous tudions une premire implmentation de cette architecture en couches.

4.3
4.3.1

Application exemple - version 4


Le projet Visual Studio

Le projet Visual Studio sera le suivant :

Architectures 3 couches, tests unitaires NUnit, framework Spring

130

5 6 2 1 3 4

[1] : le dossier [Entites] contient les objets transversaux aux couches [ui, metier, dao] : la structure TrancheImpot, l'exception FileImpotException. [2] : le dossier [Dao] contient les classes et interfaces de la couche [dao]. Nous utiliserons deux implmentations de l'interface IImpotDao : la classe HardwiredImpot tudie au paragraphe 2.10 page 84 et FileImpot tudie au paragraphe 3.8 page 121. [3] : le dossier [Metier] contient les classes et interfaces de la couche [metier] [4] : le dossier [Ui] contient les classes de la couche [ui] [5] : le fichier [DataImpot.txt] contient les tranches d'impt utilises par l'implmentation FileImpot de la couche [dao]. Il est configur [6] pour tre automatiquement recopi dans le dossier d'excution du projet.

4.3.2

Les entits de l'application

Revenons sur l'architecture 3 couches de notre application :

utilisateur

Couche ui [ui]

Couche mtier [metier]

Couche d'accs aux donnes [dao]

Donnes

Couche [entites]

Nous appelons entits les classes transversales aux couches. C'est le cas en gnral des classes et structures qui encapsulent des donnes de la couche [dao]. Ces entits remontent en gnral jusqu' la couche [ui]. Les entits de l'application sont les suivantes : La structure TrancheImpot
1. 2. 3. 4. 5. 6. 7. 8. namespace Entites { // une tranche d'impt public struct TrancheImpot public decimal Limite { public decimal CoeffR { public decimal CoeffN { } }

{ get; set; } get; set; } get; set; }

L'exception FileImpotException
1. 2. 3. 4. 5. using System; namespace Entites { public class FileImpotException : Exception { // codes d'erreur

Architectures 3 couches, tests unitaires NUnit, framework Spring

131

6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. } 22. }

[Flags] public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 }; // code d'erreur public CodeErreurs Code { get; set; } // constructeurs public FileImpotException() { } public FileImpotException(string message) : base(message) { } public FileImpotException(string message, Exception e) : base(message, e) { }

Note : la classe FileImpotException n'est utile que si la couche [dao] est implmente par la classe FileImpot.

4.3.3

La couche [dao]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites]

Couche d'accs aux donnes [dao] DLL

Donnes

Rappelons l'interface de la couche [dao] :


1. 2. 3. 4. 5. 6. 7. 8. using Entites; namespace Dao { public interface IImpotDao { // les tranches d'impt TrancheImpot[] TranchesImpot{get;} } }

Nous implmenterons cette interface de deux faons diffrentes. Tout d'abord avec la classe HardwiredImpot tudie au paragraphe 2.10 page 84 :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. using System; using Entites; namespace Dao { public class HardwiredImpot : IImpotDao { // tableaux de donnes ncessaires au calcul de l'impt decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M }; decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M }; decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M }; // tranches d'impt public TrancheImpot[] TranchesImpot { get; private set; } // constructeur public HardwiredImpot() { // cration du tableau des tranches d'impt TranchesImpot = new TrancheImpot[limites.Length]; // remplissage for (int i = 0; i < TranchesImpot.Length; i++) { TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] }; } } }// classe }// namespace

Architectures 3 couches, tests unitaires NUnit, framework Spring

132

ligne 5 : la classe HardwiredImpot implmente l'interface IImpotDao ligne 12 : implmentation de la proprit TranchesImpot de l'interface IImpotDao. Cette proprit est une proprit automatique. Elle implmente la mthode get de la proprit TranchesImpot de l'interface IImpotDao. On a de plus dclar une mthode set prive donc interne la classe afin que le constructeur des lignes 15-22 puisse initialiser le tableau des tranches d'impt.

L'interface IImpotDao sera galement implmente par la classe FileImpot tudie au paragraphe 3.8 page 121 :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. using using using using using System; System.Collections.Generic; System.IO; System.Text.RegularExpressions; Entites;

namespace Dao { class FileImpot : IImpotDao { // fichier des donnes public string FileName { get; set; } // tranches d'impt public TrancheImpot[] TranchesImpot { get; private set; } // constructeur public FileImpot(string fileName) { // on mmorise le nom du fichier FileName = fileName; // donnes List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>(); int numLigne = 1; // exception FileImpotException fe = null; // lecture contenu du fichier fileName, ligne par ligne Regex pattern = new Regex(@"s*:\s*"); // au dpart pas d'erreur FileImpotException.CodeErreurs code = 0; try { using (StreamReader input = new StreamReader(FileName)) { while (!input.EndOfStream && code == 0) { // ligne courante string ligne = input.ReadLine().Trim(); // on ignore les lignes vides if (ligne == "") continue; // ligne dcompose en trois champs spars par : string[] champsLigne = pattern.Split(ligne); // a-t-on 3 champs ? if (champsLigne.Length != 3) { code = FileImpotException.CodeErreurs.Ligne; } // conversions des 3 champs decimal limite = 0, coeffR = 0, coeffN = 0; if (code == 0) { if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1; if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2; if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ; } // erreur ? if (code != 0) { // on note l'erreur fe = new FileImpotException(String.Format("Ligne n {0} incorrecte", numLigne)) { Code = code }; } else { // on mmorise la nouvelle tranche d'impt listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN }); // ligne suivante numLigne++; } }

Architectures 3 couches, tests unitaires NUnit, framework Spring

133

65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80.

} } catch (Exception e) { // on note l'erreur fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces }; } // erreur signaler ? if (fe != null) { // on lance l'exception throw fe; } else { // on rend la liste listImpot dans le tableau tranchesImpot TranchesImpot = listTranchesImpot.ToArray(); } } } }

ce code a dj t tudi au paragraphe 3.8 page 121. ligne 14 : la mthode TranchesImpot de l'interface IImpotDao ligne 76 : initialisation des tranches d'impt dans le constructeur de la classe, partir du fichier dont le contructeur a reu le nom ligne 17.

4.3.4

La couche [metier]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites]

Couche d'accs aux donnes [dao] DLL

Donnes

Rappelons l'interface de cette couche :


1. 2. 3. 4. 5. namespace Metier { public interface IImpotMetier { int CalculerImpot(bool mari, int nbEnfants, int salaire); } }

L'implmentation ImpotMetier de cette interface est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. using Entites; using Dao; namespace Metier { public class ImpotMetier : IImpotMetier { // couche [dao] private IImpotDao Dao { get; set; } // les tranches d'impt private TrancheImpot[] tranchesImpot; // constructeur public ImpotMetier(IImpotDao dao) { // mmorisation Dao = dao; // tranches d'impt tranchesImpot = dao.TranchesImpot; } // calcul de l'impt public int CalculerImpot(bool mari, int nbEnfants, int salaire) { // calcul du nombre de parts decimal nbParts;

Architectures 3 couches, tests unitaires NUnit, framework Spring

134

25. if (mari) 26. nbParts = (decimal)nbEnfants / 2 + 2; 27. else 28. nbParts = (decimal)nbEnfants / 2 + 1; 29. if (nbEnfants >= 3) 30. nbParts += 0.5M; 31. // calcul revenu imposable & Quotient familial 32. decimal revenu = 0.72M * salaire; 33. decimal QF = revenu / nbParts; 34. // calcul de l'impt 35. tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1; 36. int i = 0; 37. while (QF > tranchesImpot[i].Limite) 38. i++; 39. // retour rsultat 40. return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN); 41. }//calculer 42. }//classe 43. 44. }

ligne 5 : la classe [Metier] implmente l'interface [IImpotMetier]. lignes 14-19 : la couche [metier] doit collaborer avec la couche [dao]. Elle doit donc avoir une rfrence sur l'objet implmentant l'interface IImpotDao. C'est pourquoi cette refrence est-elle passe en paramtre au constructeur. ligne 16 : la rfrence sur la couche [dao] est mmoris dans le champ priv de la ligne 8 ligne 18 : partir de cette rfrence, le constructeur demande le tableau des tranches d'impt et en mmorise une rfrence dans la proprit prive de la ligne 8. lignes 22-41 : implmentation de la mthode CalculerImpot de l'interface IImpotMetier. Cette implmentation utilise le tableau des tranches d'impt initialis par le constructeur.

4.3.5

La couche [ui]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites]

Couche d'accs aux donnes [dao] DLL

Donnes

Les classes de dialogue avec l'utilisateur des versions 2 et 3 taient trs proches. Celle de la version 2 tait la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. using System; namespace Chap2 { public class Program { static void Main() { ... // cration d'un objet IImpot IImpot impot = new HardwiredImpot(); // boucle infinie while (true) { } } } }//while

...

et celle de la version 3 :
1. 2. 3. using System; namespace Chap3 {

Architectures 3 couches, tests unitaires NUnit, framework Spring

135

4. public class Program { 5. static void Main() { 6. ... 7. 8. // cration d'un objet IImpot 9. IImpot impot = null; 10. try { 11. // cration d'un objet IImpot 12. impot = new FileImpot("DataImpotInvalide.txt"); 13. } catch (FileImpotException e) { 14. // affichage erreur 15. string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); 16. Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); 17. // arrt programme 18. Environment.Exit(1); 19. } 20. // boucle infinie 21. while (true) { 22. ... 23. }//while 24. } 25. } 26. }

Seule change la faon d'instancier l'objet de type IImpot qui permet le calcul de l'impt. Cet objet correspond ici notre couche [mtier]. Pour une implmentation [dao] avec la classe HardwiredImpot, la classe de dialogue est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. using using using using System; Metier; Dao; Entites;

namespace Ui { public class Dialogue2 { static void Main() { ... // on cre les couches [metier et dao] IImpotMetier metier = new ImpotMetier(new HardwiredImpot()); // boucle infinie while (true) { ... // les paramtres sont corrects - on calcule l'Impot Console.WriteLine("Impot=" + metier.CalculerImpot(mari == "o", nbEnfants, salaire) + "

euros"); 19. // contribuable suivant 20. }//while 21. } 22. } 23. }

ligne 12 : instanciation des couches [dao] et [metier]. On rappelle que la couche [metier] a besoin de la couche [dao]. ligne 18 : utilisation de la couche [metier] pour calculer l'impt

Pour une implmentation [dao] avec la classe FileImpot, la classe de dialogue est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. using using using using System; Metier; Dao; Entites;

namespace Ui { public class Dialogue { static void Main() { ... // on cre les couches [metier et dao] IImpotMetier metier = null; try {

Architectures 3 couches, tests unitaires NUnit, framework Spring

136

13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31.

// cration couche [metier] metier = new ImpotMetier(new FileImpot("DataImpot.txt")); } catch (FileImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); // arrt programme Environment.Exit(1); } // boucle infinie while (true) { ... // les paramtres sont corrects - on calcule l'Impot Console.WriteLine("Impot=" + metier.CalculerImpot(mari == "o", nbEnfants, salaire) + " euros"); // contribuable suivant }//while } } }

ligne 11-21 : instanciation des couches [dao] et [metier]. L'instanciation de la couche [dao] pouvant lancer une exception, celle-ci est gre ligne 26 : utilisation de la couche [metier] pour calculer l'impt, comme dans la version prcdente

4.3.6

Conclusion

L'architecture en couches et l'utilisation d'interfaces a amen une certaine souplesse notre application. Celle-ci apparat notamment dans la faon dont la couche [ui] instancie les couches [dao] et [mtier] :
1. 2. // on cre les couches [metier et dao] IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

dans un cas et :
1. 2. 3. 4. 5. 6. 7. 8. // on cre les couches [metier et dao] IImpotMetier metier = null; try { // cration couche [metier] metier = new ImpotMetier(new FileImpot("DataImpot.txt")); } catch (FileImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); 9. Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); 10. // arrt programme 11. Environment.Exit(1); 12. }

dans l'autre. Si on excepte la gestion de l'exception dans le cas 2, l'instanciation des couches [dao] et [metier] est similaire dans les deux applications. Une fois les couches [dao] et [metier] instancies, le code de la couche [ui] est identique dans les deux cas. Ceci est du au fait que la couche [mtier] est manipule via son interface IImpotMetier et non via la classe d'implmentation de celle-ci. Changer la couche [metier] ou la couche [dao] de l'application sans changer leurs interfaces reviendra toujours changer les seules lignes prcdentes dans la couche [ui]. Un autre exemple de souplesse amene par cette architecture est celui de l'implmentation de la couche [mtier] :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using Entites; using Dao; namespace Metier { public class ImpotMetier : IImpotMetier { // couche [dao] private IImpotDao Dao { get; set; } // les tranches d'impt

Architectures 3 couches, tests unitaires NUnit, framework Spring

137

11. private TrancheImpot[] tranchesImpot; 12. 13. // constructeur 14. public ImpotMetier(IImpotDao dao) { 15. // mmorisation 16. Dao = dao; 17. // tranches d'impt 18. tranchesImpot = dao.TranchesImpot; 19. } 20. 21. // calcul de l'impt 22. public int CalculerImpot(bool mari, int nbEnfants, int salaire) { 23. ... 24. }//calculer 25. }//classe 26. 27. }

Ligne 14, on voit que la couche [mtier] est construite partir d'une rfrence sur l'interface de la couche [dao]. Changer l'implmentation de cette dernire a donc un impact zro sur la couche [mtier]. C'est pour cela, que notre unique implmentation de la couche [mtier] a pu travailler sans modifications avec deux implmentations diffrentes de la couche [dao].

4.4

Application exemple - version 5

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

Cette nouvelle version reprend la prcdente en y apportant les modifications suivantes : les couches [mtier] et [dao] sont chacune encapsules dans une DLL et teste avec le framework de tests unitaires NUnit. l'intgration des couches est assure par le framework Spring Dans les grands projets, plusieurs dveloppeurs travaillent sur le mme projet. Les architectures en couches facilitent ce mode de travail : parce que les couches communiquent entre-elles avec des interfaces bien dfinies, un dveloppeurs travaillant sur une couche n'a pas se proccuper du travail des autres dveloppeurs sur les autres couches. Il suffit juste que tout le monde respecte les interfaces. Ci-dessus, le dveloppeur de la couche [mtier] aura besoin au moment des tests de sa couche d'une implmentation de la couche [dao]. Tant que celle-ci n'est pas termine, il peut utiliser une implmentation factice de la couche [dao] tant qu'elle respecte l'interface IImpotDao. C'est l galement un avantage de l'architecture en couches : un retard dans la couche [dao] n'empche pas les tests de la couche [mtier]. L'implmentation factice de la couche [dao] a galement l'avantage d'tre bien souvent plus facile mettre en oeuvre que la vritable couche [dao] qui peut ncessiter de lancer un SGBD, d'avoir des connexions rseau, ... Lorsque la couche [dao] est termine et teste, elle sera fournie aux dveloppeurs de la couche [mtier] sous la forme d'une DLL plutt que de code source. Au final, l'application est souvent dlivre sous la forme d'un excutable .exe (celui de la couche [ui]) et de bibliothques de classes .dll (les autres couches).

4.4.1

NUnit

Les tests faits jusqu' maintenant pour nos diverses applications reposaient sur une vrification visuelle. On vrifiait qu'on obtenait l'cran ce qui tait attendu. C'est une mthode inutilisable lorsqu'il y a de nombreux tests faire. L'tre humain est en effet sujet la fatigue et sa capacit vrifier des tests s'mousse au fil de la journe. Les tests doivent alors tre automatiss et viser ne ncessiter aucune intervention humaine.

Architectures 3 couches, tests unitaires NUnit, framework Spring

138

Une application volue au fil du temps. A chaque volution, on doit vrifier que l'application ne "rgresse" pas, c.a.d. qu'elle continue passer les tests de bon fonctionnement qui avaient t faits lors de son criture initiale. On appelle ces tests, des tests de "non rgression". Une application un peu importante peut ncessiter des centaines de tests. On teste en effet chaque mthode de chaque classe de l'application. On appelle cela des tests unitaires. Ceux-ci peuvent mobiliser beaucoup de dveloppeurs s'ils n'ont pas t automatiss. Des outils ont t dvelopps pour automatiser les tests. L'un d'eux s'appelle NUnit. Il est disponible sur le site [http://www.nunit.org] :

C'est la version 2.4.6 ci-dessus qui a t utilise pour ce document (mars 2008). L'installation place une icne [1] sur le bureau :

2 1

Un double-clic sur l'icne [1] lance l'interface graphique de NUnit [2]. Celle-ci n'aide en rien l'automatisation des tests puisque de nouveau nous sommes ramens une vrification visuelle : le testeur vrifie les rsultats des tests affichs dans l'interface graphique. Nanmoins les tests peuvent tre galement excuts par des outils batch et leurs rsultats enregistrs dans des fichiers XML. C'est cette mthode qui est utilise par les quipes de dveloppement : les tests sont lancs la nuit et les dveloppeurs ont le rsultat le lendemain matin. Examinons avec un exemple le principe des tests NUnit. Tout d'abord, crons un nouveau projet C# de type Console Application :

En [1], on voit les rfrences du projet. Ces rfrences sont des DLL contenant des classes et interfaces utilises par le projet. Celles prsentes en [1] sont incluses par dfaut dans chaque nouveau projet C#. Pour pouvoir utiliser les classes et interfaces du framework NUnit, il nous faut ajouter [2] une nouvelle rfrence au projet.

Architectures 3 couches, tests unitaires NUnit, framework Spring

139

4 3

Dans l'onglet .NET ci-dessus, nous choisissons le composant [nunit.framework]. Les composants [nunit.*] ci-dessus ne sont pas des composants prsents par dfaut dans l'environnement .NET. Ils y ont t amens par l'installation prcdente du framework NUnit. Une fois l'ajout de la rfrence valide, celle-ci apparat [4] dans la liste des rfrences du projet. Avant gnration de l'application, le dossier [bin/Release] du projet est vide. Aprs gnration (F6), on peut constater que le dossier [bin/Release] n'est plus vide :

En [6], on voit la prsence de la DLL [nunit.framework.dll]. C'est l'ajout de la rfrence [nunit.framework] qui a provoqu la copie de cette DLL dans le dossier d'excution. Celui-ci est en effet l'un des dossiers qui seront explors par le CLR (Common Language Runtime) .NET pour trouver les classes et interfaces rfrences par le projet. Construisons une premire classe de test NUnit. Pour cela, nous supprimons la classe [Program.cs] gnre par dfaut puis nous ajoutons une nouvelle classe [Nunit1.cs] au projet. Nous supprimons galement les rfrences inutiles [7]. La classe de test NUnit1 sera la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. using System; using NUnit.Framework; namespace NUnit { [TestFixture] public class NUnit1 { public NUnit1() { Console.WriteLine("constructeur"); } [SetUp] public void avant() { Console.WriteLine("Setup"); } [TearDown] public void aprs() { Console.WriteLine("TearDown"); } [Test] public void t1() { Console.WriteLine("test1"); Assert.AreEqual(1, 1); } [Test] public void t2() { Console.WriteLine("test2"); Assert.AreEqual(1, 2, "1 n'est pas gal 2"); } } }

Architectures 3 couches, tests unitaires NUnit, framework Spring

140

ligne 6 : la classe NUnit1 doit tre publique. Le mot cl public n'est pas gnr par dfaut par Visual Studio. Il faut le rajouter. ligne 5 : l'attribut [TestFixture] est un attribut NUnit. Il indique que la classe est une classe de test. lignes 7-9 : le constructeur. Il n'est utilis ici que pour crire un message l'cran. On veut voir quand il est excut. ligne 10 : l'attribut [SetUp] dfinit une mthode excute avant chaque test unitaire. ligne 14 : l'attribut [TearDown] dfinit une mthode excut aprs chaque test unitaire. ligne 18 : l'attribut [Test] dfinit une mthode de test. Pour chaque mthode annote avec l'attribut [Test], la mthode annote [SetUp] sera excute avant le test et la mthode annote [TearDown] sera excute aprs le test. ligne 21 : l'une des mthodes [Assert.*] dfinies par le framework NUnit. On trouve les mthodes [Assert] suivantes : [Assert.AreEqual(expression1, expression2)] : vrifie que les valeurs des deux expressions sont gales. De nombreux types d'expression sont accepts (int, string, float, double, decimal, ...). Si les deux expressions ne sont pas gales, alors une exception est lance. [Assert.AreEqual(rel1, rel2, delta)] : vrifie que deux rels sont gaux delta prs, c.a.d abs(rel1-rel2)<=delta. On pourra crire par exemple [Assert.AreEqual(rel1, rel2, 1E-6)] pour vrifier que deux valeurs sont gales 10-6 prs. [Assert.AreEqual(expression1, expression2, message)] et [Assert.AreEqual(rel1, rel2, delta, message)] sont des variantes permettant de prciser le message d'erreur associer l'exception lance lorsque la mthode [Assert.AreEqual] choue. [Assert.IsNotNull(object)] et [Assert.IsNotNull(object, message)] : vrifie que object n'est pas gal null. [Assert.IsNull(object)] et [Assert.IsNull(object, message)] : vrifie que object est gal null. [Assert.IsTrue(expression)] et [Assert.IsTrue(expression, message)] : vrifie que expression est gale true. [Assert.IsFalse(expression)] et [Assert.IsFalse(expression, message)] : vrifie que expression est gale false. [Assert.AreSame(object1, object2)] et [Assert.AreSame(object1, object2, message)] : vrifie que les rfrences object1 et object2 dsignent le mme objet. [Assert.AreNotSame(object1, object2)] et [Assert.AreNotSame(object1, object2, message)] : vrifie que les rfrences object1 et object2 ne dsignent pas le mme objet.

ligne 21 : l'assertion doit russir ligne 26 : l'assertion doit chouer

Configurons le projet afin que sa gnration produise une DLL plutt qu'un excutable .exe : 2

4 3

en [1] : proprits du projet en [2, 3] : comme type de projet, on choisit [Class Library] (Bibliothque de classes) en [4] : la gnration du projet produira une DLL (assembly) appele [Nunit.dll]

Utilisons maintenant, NUnit pour excuter la classe de test :

Architectures 3 couches, tests unitaires NUnit, framework Spring

141

2 3 5 1

4 6

en [1] : ouverture d'un projet NUnit en [2, 3] : on charge la DLL bin/Release/Nunit.dll produite par la gnration du projet C# en [4] : la DLL a t charge en [5] : l'arbre des tests en [6] : on les excute

en [7] : les rsultats : t1 a russi, t2 a chou en [8] : une barre rouge indique l'chec global de la classe de tests en [9] : le message d'erreur li au test rat 12 11

en [11] : les diffrents onglets de la fentre des rsultats en [12] : l'onglet [Console.Out]. On y voit que : le constructeur n'a t excut qu'une fois la mthode [SetUp] a t excute avant chacun des deux tests la mthode [TearDown] a t excute aprs chacun des deux tests

Il est possible de prciser les mthodes tester :

Architectures 3 couches, tests unitaires NUnit, framework Spring

142

en [1] : on demande l'affichage d'une case cocher ct de chaque test en [2] : on coche les tests excuter en [3] : on les excute

Pour corriger les erreurs, il suffit de corriger le projet C# et de le rgnrer. NUnit dtecte que la DLL qu'il teste a t change et charge la nouvelle automatiquement. Il suffit alors de relancer les tests. Considrons la nouvelle classe de test suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. using System; using NUnit.Framework; namespace NUnit { [TestFixture] public class NUnit2 : AssertionHelper { public NUnit2() { Console.WriteLine("constructeur"); } [SetUp] public void avant() { Console.WriteLine("Setup"); } [TearDown] public void aprs() { Console.WriteLine("TearDown"); } [Test] public void t1() { Console.WriteLine("test1"); Expect(1, EqualTo(1)); } [Test] public void t2() { Console.WriteLine("test2"); Expect(1, EqualTo(2), "1 n'est pas gal 2"); } } }

A partir de la version 2.4 de NUnit, une nouvelle syntaxe est devenue disponible, celles des lignes 21 et 26. Pour cela, la classe de test doit driver de la classe AssertionHelper (ligne 6). La correspondance (non exhaustive) entre ancienne et nouvelle syntaxe est la suivante :
Assert.AreEqual(expression1, expression2, message) Expect(expression1,EqualTo(expression2),message)

Assert.AreEqual(rel1, rel2, delta, message) Expect(expression1,EqualTo(expression2).Within(delta),mess age) Assert.AreSame(objet1, objet2, message) Assert.AreNotSame(objet1, objet2, message) Assert.IsNull(objet,message) Assert.IsNotNull(objet,message) Assert.IsTrue(expression,message) Assert.IsFalse(expression,message) Expect(objet1,SameAs(objet2),message) Expect(objet1,Not.SameAs(objet2),message) Expect(objet,Null,message) Expect(objet,Not.Null,message) Expect(expression,True,message) Expect(expression,False,message)

Architectures 3 couches, tests unitaires NUnit, framework Spring

143

Ajoutons le test suivant la classe NUnit2 :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. } [Test] public void t3() { bool vrai = true, faux = false; Expect(vrai, True); Expect(faux, False); Object obj1 = new Object(), obj2 = null, obj3=obj1; Expect(obj1, Not.Null); Expect(obj2, Null); Expect(obj3, SameAs(obj1)); double d1 = 4.1, d2 = 6.4, d3 = d1; Expect(d1, EqualTo(d3).Within(1e-6)); Expect(d1, Not.EqualTo(d2));

Si on gnre (F6) la nouvelle DLL du projet C#, le projet NUnit devient le suivant :

3 1 2

en [1] : la nouvelle classe de test [NUnit2] a t automatiquement dtecte en [2] : on excute le test t3 de NUnit2 en [3] : le test t3 a t russi

Pour approfondir NUnit, on lira l'aide de NUnit :

4.4.2

La solution Visual Studio

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

Architectures 3 couches, tests unitaires NUnit, framework Spring

144

Nous allons construire progressivement la solution Visual Studio suivante :

3 2 4

en [1] : la solution ImpotsV5 est forme de trois projets, un pour chacune des trois couches de l'application en [2] : le projet [dao] de la couche [dao] en [3] : le projet [metier] de la couche [metier] en [4] : le projet [ui] de la couche [ui]

La solution ImpotsV5 peut tre construite de la faon suivante :

2 1 3 4 5

en [1] : crer un nouveau projet en [2] : choisir une application console en [3] : appeler le projet [dao] en [4] : crer le projet en [5] : une fois le projet cr, le sauvegarder

6 7 8 9 10 11

Architectures 3 couches, tests unitaires NUnit, framework Spring

145

en [6] : garder le nom [dao] pour le projet en [7] : prciser un dossier pour enregistrer le projet et sa solution en [8] : donner un nom la solution en [9] : indiquer que la solution doit avoir son propre dossier en [10] : enregistrer le projet et sa solution en [11] : le projet [dao] dans sa solution ImpotsV5

12 13 14

en [12] : le dossier de la solution ImpotsV5. Il contient le dossier [dao] du dossier [dao]. en [13] : le contenu du dossier [dao] en [14] : on ajoute un nouveau projet la solution ImpotsV5

15

16 17

en [15] : le nouveau projet s'appelle [metier] en [16] : la solution avec ses deux projets en [17] : la solution, une fois qu'on lui a ajout le 3ime projet [ui]

Architectures 3 couches, tests unitaires NUnit, framework Spring

146

19

18

21

20

en [18] : le dossier de la solution et les dossiers des trois projets lorsqu'on excute une solution par (Ctrl+F5), c'est le projet actif qui est excut. Il en est de mme lorsqu'on gnre (F6) la solution. Le nom du project actif est en gras [19] dans la solution. en [20] : pour changer le projet actif de la solution en [21] : le projet [metier] est dsormais le projet actif de la solution

4.4.3

La couche [dao]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

1 3 2 4 5

Architectures 3 couches, tests unitaires NUnit, framework Spring

147

Les rfrences du projet (cf [1] dans le projet) On ajoute la rfrence [nunit.framework] ncessaire aux tests [NUnit] Les entits (cf [2] dans le projet) La classe [TrancheImpot] est celle des versions prcdentes. La classe [FileImpotException] de la version prcdente est renomme en [ImpotException] pour la rendre plus gnrique et ne pas la lier une couche [dao] particulire :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System; namespace Entites { public class ImpotException : Exception { // code d'erreur public int Code { get; set; } // constructeurs public ImpotException() { } public ImpotException(string message) : base(message) { } public ImpotException(string message, Exception e) : base(message, e) { }

} }

La couche [dao] (cf [3] dans le projet) L'interface [IImpotDao] est celle de la version prcdente. Il en est de mme pour la classe [HardwiredImpot]. La classe [FileImpot] volue pour tenir compte du changement de l'exception [FileImpotException] en [ImpotException] :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. ... namespace Dao { public class FileImpot : IImpotDao { // codes d'erreur [Flags] public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 }; ... // constructeur public FileImpot(string fileName) { // on mmorise le nom du fichier FileName = fileName; ... // au dpart pas d'erreur CodeErreurs code = 0; try { using (StreamReader input = new StreamReader(FileName)) { while (!input.EndOfStream && code == 0) {

...

// erreur ? if (code != 0) { // on note l'erreur fe = new ImpotException(String.Format("Ligne n {0} incorrecte", numLigne)) { Code = (int)code }; } else { ... } } } } catch (Exception e) { // on note l'erreur fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces }; } // erreur signaler ? ...

Architectures 3 couches, tests unitaires NUnit, framework Spring

148

38. 39. } 40. }

ligne 8 : les codes d'erreurs auparavant dans la classe [FileImpotException] ont migr dans la classe [FileImpot]. Ce sont en effet des codes d'erreur spcifiques cette implmentation de l'interface [IImpotDao]. lignes 26 et 34 : pour encapsuler une erreur, c'est la classe [ImpotException] qui est utilise et non plus la classe [FileImpotException].

Le test [Test1] (cf [4] dans le projet) La classe [Test1] se contente d'afficher les tranches d'impt l'cran :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; using Dao; using Entites; namespace Tests { class Test1 { static void Main() { // on cre la couche [dao] IImpotDao dao = null; try { // cration couche [dao] dao = new FileImpot("DataImpot.txt"); } catch (ImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); // arrt programme Environment.Exit(1); } // on affiche les tranches d'impt TrancheImpot[] tranchesImpot = dao.TranchesImpot; foreach (TrancheImpot t in tranchesImpot) { Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN); } } } }

ligne 13 : la couche [dao] est implmente par la classe [FileImpot] ligne 14 : on gre l'exception de type [ImpotException] qui peut survenir.

Le fichier [DataImpot.txt] ncessaire aux tests est recopi automatiquement dans le dossier d'excution du projet (cf [5] dans le projet). Le projet [dao] va avoir plusieurs classes contenant une mthode [Main]. Il faut indiquer alors explicitement la classe excuter lorsque l'utilisateur demande l'excution du project par Ctrl-F5 :

Architectures 3 couches, tests unitaires NUnit, framework Spring

149

2 3

en [1] : accder aux proprits du projet en [2] : prciser que c'est une application console en [3] : prciser la classe excuter

L'excution de la classe [Test1] prcdente donne les rsultats suivants :


4962:0:0 8382:0,068:291,09 14753:0,191:1322,92 23888:0,283:2668,39 38868:0,374:4846,98 47932:0,426:6883,66 0:0,481:9505,54

Le test [Test2] (cf [4] dans le projet) La classe [Test2] fait la mme chose que la classe [Test1] en implmentant la couche [dao] avec la classe [HardwiredImpot]. La ligne 13 de [Test1] est remplace par la suivante :
dao = new HardwiredImpot();

Le projet est modifi pour excuter dsormais la classe [Test2] :

1 2

Les rsultats cran sont les mmes que prcdemment. Le test NUnit [NUnit1] (cf [4] dans le projet) Le test unitaire [NUnit1] est le suivant :

Architectures 3 couches, tests unitaires NUnit, framework Spring

150

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33.

using using using using

System; Dao; Entites; NUnit.Framework;

namespace Tests { [TestFixture] public class NUnit1 : AssertionHelper{ // couche [dao] tester private IImpotDao dao; // constructeur public NUnit1() { // initialisation couche [dao] dao = new FileImpot("DataImpot.txt"); } // test [Test] public void ShowTranchesImpot(){ // on affiche les tranches d'impt TrancheImpot[] tranchesImpot = dao.TranchesImpot; foreach (TrancheImpot t in tranchesImpot) { Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN); } // qqs tests Expect(tranchesImpot.Length,EqualTo(7)); Expect(tranchesImpot[2].Limite,EqualTo(14753)); Expect(tranchesImpot[2].CoeffR, EqualTo(0.191)); Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92)); } } }

la classe de test drive de la classe [AssertionHelper], ce qui permet l'utilisation de la mthode statique Expect (lignes 27-30). ligne 10 : une rfrence sur la couche [dao] lignes 13-16 : le constructeur instancie la couche [dao] avec la classe [FileImpot] lignes 19-20 : la mthode de test ligne 22 : on rcupre le tableau des tranches d'impt auprs de la couche [dao] lignes 23-25 : on les affiche comme prcdemment. Cet affichage n'aurait pas lieu d'tre dans un test unitaire rel. Ici, cet affichage a un souci pdagogique. lignes 27 : on vrifie qu'il y a bien 7 tranches d'impt lignes 28-30 : on vrifie les valeurs de la tranche d'impt n 2

Pour excuter ce test unitaire, le projet doit tre de type [Class Library] :

2 1 3

en [1] : la nature du projet a t change en [2] : la DLL gnre s'appellera [ImpotsV5-dao.dll] en [3] : aprs gnration (F6) du projet, le dossier [dao/bin/Release] contient la DLL [ImpotsV5-dao.dll]

La DLL [ImpotsV5-dao.dll] est ensuite charge dans le framework NUnit et excute :

Architectures 3 couches, tests unitaires NUnit, framework Spring

151

2 1 3

en [1] : les tests ont t russis. Nous considrons dsormais la couche [dao] oprationnelle. Sa DLL contient toutes les classes du projet dont les classes de test. Celles-ci sont inutiles. Nous reconstruisons la DLL afin d'en exclure les classes de tests. en [2] : le dossier [tests] est exclu du projet en [3] : le nouveau projet. Celui-ci est rgnr par F6 afin de gnrer une nouvelle DLL.

4.4.4

La couche [metier]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

6 2 1 3 4 5 7

en [1], le projet [metier] est devenu le projet actif de la solution en [2] : les rfrences du projet en [3] : la couche [metier] en [4] : les classes de test en [5] : le fichier [DataImpot.txt] des tranches d'impt configur [6] pour tre recopi automatiquement dans le dossier d'excution du projet [7]

Les rfrences du projet (cf [2] dans le projet) Comme pour le projet [dao], on ajoute la rfrence [nunit.framework] ncessaire aux tests [NUnit]. La couche [metier] a besoin de la couche [dao]. Il lui faut donc une rfrence sur la DLL de cette couche. On procde ainsi :

Architectures 3 couches, tests unitaires NUnit, framework Spring

152

2 1 3 4

en [1] : on ajoute une nouvelle rfrence aux rfrences du projet [metier] en [2] : on slectionne l'onglet [Browse] en [3] : on slectionne le dossier [dao/bin/Release] en [4] : on slectionne la DLL [ImpotsV5-dao.dll] gnre dans le projet [dao] en [5] : la nouvelle rfrence

La couche [metier] (cf [3] dans le projet) L'interface [IImpotMetier] est celle de la version prcdente. Il en est de mme pour la classe [ImpotMetier]. Le test [Test1] (cf [4] dans le projet) La classe [Test1] se contente de faire quelques calculs de salaire :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. using using using using System; Dao; Entites; Metier;

namespace Tests { class Test1 { static void Main() { // on cre la couche [metier] IImpotMetier metier = null; try { // cration couche [metier] metier = new ImpotMetier(new FileImpot("DataImpot.txt")); } catch (ImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); // arrt programme Environment.Exit(1); } // on calcule qqs impots Console.WriteLine(String.Format("Impot(true,2,60000)={0} euros", metier.CalculerImpot(true, 2, 60000))); Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 60000))); Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 6000))); Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 600000))); } } }

ligne 14 : cration des couches [metier] et [dao]. La couche [dao] est implmente avec la classe [FileImpot] lignes 12-21 : gestion d'une ventuelle exception de type [ImpotException] lignes 23-26 : appels rpts de l'unique mthode CalculerImpot de l'interface [IImpotMetier].

Le projet [metier] est configur comme suit :

Architectures 3 couches, tests unitaires NUnit, framework Spring

153

1 2

[1] : le projet est de type application console [2] : la classe excute est la classe [Test1] [3] : la gnration du projet produira l'excutable [ImpotsV5-metier.exe]

L'excution du projet donne les rsultats suivants : 1. 2. 3. 4. Impot(true,2,60000)=4282 euros Impot(false,3,60000)=4282 euros Impot(false,3,60000)=0 euros Impot(false,3,60000)=179275 euros

Le test [NUnit1] (cf [4] dans le projet) La classe de tests unitaires [NUnit1] reprend les quatre calculs prcdents et en vrifie le rsultat :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. using Dao; using Metier; using NUnit.Framework; namespace Tests { [TestFixture] public class NUnit1:AssertionHelper { // couche [metier] tester private IImpotMetier metier; // constructeur public NUnit1() { // initialisation couche [metier] metier = new ImpotMetier(new FileImpot("DataImpot.txt")); } // test [Test] public void CalculsImpot(){ // on affiche les tranches d'impt Expect(metier.CalculerImpot(true, 2, 60000), EqualTo(4282)); Expect(metier.CalculerImpot(false, 3, 60000), EqualTo(4282)); Expect(metier.CalculerImpot(false, 3, 6000), EqualTo(0)); Expect(metier.CalculerImpot(false, 3, 600000), EqualTo(179275)); }

} }

ligne 14 : cration des couches [metier] et [dao]. La couche [dao] est implmente avec la classe [FileImpot] lignes 21-24 : appels rpts de l'unique mthode CalculerImpot de l'interface [IImpotMetier] avec vrification des rsultats.

Le projet [metier] est maintenant configur comme suit :

Architectures 3 couches, tests unitaires NUnit, framework Spring

154

[1] : le projet est de type "bibliothque de classes" [2] : la gnration du projet produira la DLL [ImpotsV5-metier.dll]

Le projet est gnr (F6). Puis la DLL [ImpotsV5-metier.dll] gnre est charge dans NUnit et teste :

Ci-dessus, les tests ont t russis. Nous considrons dsormais la couche [metier] oprationnelle. Sa DLL contient toutes les classes du projet dont les classes de test. Celles-ci sont inutiles. Nous reconstruisons la DLL afin d'en exclure les classes de tests.

2 1

en [1] : le dossier [tests] est exclu du projet en [2] : le nouveau projet. Celui-ci est rgnr par F6 afin de gnrer une nouvelle DLL.

4.4.5

La couche [ui]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

Architectures 3 couches, tests unitaires NUnit, framework Spring

155

5 2 1 4 3 6

en [1], le projet [ui] est devenu le projet actif de la solution en [2] : les rfrences du projet en [3] : la couche [ui] en [4] : le fichier [DataImpot.txt] des tranches d'impt, configur [5] pour tre recopi automatiquement dans le dossier d'excution du projet [6]

Les rfrences du projet (cf [2] dans le projet) La couche [ui] a besoin des couches [metier] et [dao] pour mener bien ses calculs d'impt. Il lui faut donc une rfrence sur les DLL de ces deux couches. On procde comme il a t montr pour la couche [metier] La classe principale [Dialogue.cs] (cf [3] dans le projet) La classe [Dialogue.cs] est celle de la version prcdente. Tests Le projet [ui] est configur comme suit :

1 3

[1] : le projet est de type "application console" [2] : la gnration du projet produira l'excutable [ImpotsV5-ui.exe] [3] : la classe qui sera excute

Un exemple d'excution (Ctrl+F5) est le suivant :


Paramtres du calcul de l'Impot au format : Mari (o/n) NbEnfants Salaire ou rien pour arrter :o 2 60000 Impot=4282 euros

4.4.6

La couche [Spring]

Revenons au code dans [Dialogue.cs] qui cre les couches [dao] et [metier] :
1. 2. 3. // on cre les couches [metier et dao] IImpotMetier metier = null; try {

Architectures 3 couches, tests unitaires NUnit, framework Spring

156

4. 5. 6. 7. 8. ... 9. 10. 11.

// cration couche [metier] metier = new ImpotMetier(new FileImpot("DataImpot.txt")); } catch (ImpotException e) { // affichage erreur // arrt programme Environment.Exit(1); }

La ligne 5 cre les couches [dao] et [metier] en nommant explicitement les classes d'implmentation des deux couches : FileImpot pour la couche [dao], ImpotMetier pour la couche [metier]. Si l'implmentation d'une des couches est faite avec une nouvelle classe, la ligne 5 sera change. Par exemple :
metier = new ImpotMetier(new HardwiredImpot());

En-dehors de ce changement, rien ne changera dans l'application du fait que chaque couche communique avec la suivante selon une interface. Tant que cette dernire ne change pas, la communication entre couches ne change pas non plus. Le framework Spring nous permet d'aller un peu plus loin dans l'indpendance des couches en nous permettant d'externaliser dans un fichier de configuration le nom des classes implmentant les diffrentes couches. Changer l'implmentation d'une couche revient alors changer un fichier de configuration. Il n'y a aucun impact sur le code de l'application.

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL 3 2 SPRING

Couche d'accs aux donnes [dao] DLL 1

Donnes

Ci-dessus, la couche [ui] va demander [0] Spring d'instancier les couches [dao] [1] et [metier] [2] d'aprs les informations contenues dans un fichier de configuration. La couche [ui] demandera ensuite Spring [3], une rfrence sur la couche [metier] :
1. 2. 3. 4. 5. 6. 7. 8. 9. ... 10. } // on cre les couches [metier et dao] IImpotMetier metier = null; try { // contexte Spring IApplicationContext ctx = ContextRegistry.GetContext(); // on demande une rfrence sur la couche [metier] metier = (IImpotMetier)ctx.GetObject("metier"); } catch (Exception e1) {

ligne 5 : instanciation des couches [dao] et [metier] par Spring ligne 7 : on rcupre une rfrence sur la couche [metier]. On notera que la couche [ui] a eu cette rfrence sans donner le nom de la classe implmentant la couche [metier].

Le framework Spring existe en deux versions : Java et .NET. La version .NET est disponible l'url (mars 2008) [http://www.springframework.net/] :

Architectures 3 couches, tests unitaires NUnit, framework Spring

157

en [1] : le site de [Spring.net] en [2] : la page des tlchargements

en [3] : tlcharger Spring 1.1 (mars 2008)

Architectures 3 couches, tests unitaires NUnit, framework Spring

158

6 7 4 5

en [4] : tlcharger la version .exe puis l'installer en [5] : le dossier gnr par l'installation en [6] : le dossier [bin/net/2.0/release] contient les DLL de Spring pour les projets Visual Studio .NET 2.0 ou suprieur. Spring est un framework riche. L'aspect de Spring que nous allons utiliser ici pour grer l'intgration des couches dans une application s'appelle IoC : Inversion of Control ou encore DI : Dependence Injection. Spring apporte des bibliothques pour l'accs aux bases de donnes avec NHibernate, la gnration et l'exploitation de services web, d'applications web, ... les DLL ncessaires pour grer l'intgration des couches dans une application sont les DLL [7] et [8].

Nous stockons ces trois DLL dans un dossier [lib] de notre projet : 2 3

1 4

[1] : les trois DLL sont places dans le dossier [lib] avec l'explorateur windows [2] : dans le projet [ui], on fait afficher tous les fichiers [3] : le dossier [ui/lib] est dsormais visible. On l'inclut dans le projet [4] : le dossier [ui/lib] fait partie du projet

L'opration de cration du dossier [lib] n'est nullement indispensable. Les rfrences pouvaient tre cres directement sur les trois DLL du dossier [bin/net/2.0/release] de [Spring.net]. La cration du dossier [lib] permet cependant de dvelopper l'application sur un poste ne disposant pas de [Spring.net] la rendant ainsi moins dpendante de l'environnement de dveloppement disponible.

Architectures 3 couches, tests unitaires NUnit, framework Spring

159

Nous ajoutons au projet [ui] des rfrences sur les trois nouvelles DLL :

3 2

[1] : on cre des rfrences sur les trois DLL du dossier [lib] [2] [3] : les trois DLL font partie des rfrences du projet

Revenons une vue d'ensemble de l'architecture de l'application :

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL 3 2 SPRING

Couche d'accs aux donnes [dao] DLL 1

Donnes

Ci-dessus, la couche [ui] va demander [0] Spring d'instancier les couches [dao] [1] et [metier] [2] d'aprs les informations contenues dans un fichier de configuration. La couche [ui] demandera ensuite Spring [3], une rfrence sur la couche [metier]. Cela se traduira dans la couche [ui] par le code suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. ... 10. } // on cre les couches [metier et dao] IImpotMetier metier = null; try { // contexte Spring IApplicationContext ctx = ContextRegistry.GetContext(); // on demande une rfrence sur la couche [metier] metier = (IImpotMetier)ctx.GetObject("metier"); } catch (Exception e1) {

ligne 5 : instanciation des couches [dao] et [metier] par Spring ligne 7 : on rcupre une rfrence sur la couche [metier].

La ligne [5] ci-dessus exploite le fichier de configuration [App.config] du projet Visual Studio. Dans un projet C#, ce fichier sert configurer l'application. [App.config] n'est donc pas une notion Spring mais une notion Visual Studio que Spring exploite. Spring sait exploiter d'autres fichiers de configuration que [App.config]. La solution prsente ici n'est donc pas la seule disponible. Crons le fichier [App.config] avec l'assistant Visual Studio :

Architectures 3 couches, tests unitaires NUnit, framework Spring

160

1 3

en [1] : ajout d'un nouvel lment au projet en [2] : slectionner "Application Configuration File" en [3] : [App.config] est le nom par dfaut de ce fichier de configuration en [4] : le fichier [App.config] a t ajout au projet

Le contenu du fichier [App.config] est le suivant :


1. 2. 3. <?xml version="1.0" encoding="utf-8" ?> <configuration> </configuration>

[App.config] est un fichier XML. La configuration du projet se fait entre les balises <configuration>. La configuration ncessaire Spring est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" /> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" /> </sectionGroup> </configSections> <spring> <context> <resource uri="config://spring/objects" /> </context> <objects xmlns="http://www.springframework.net"> <object name="dao" type="Dao.FileImpot, ImpotsV5-dao"> <constructor-arg index="0" value="DataImpot.txt"/> </object> <object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier"> <constructor-arg index="0" ref="dao"/> </object> </objects> </spring> </configuration>

lignes 11-23 : la section dlimite par la balise <spring> est appele le groupe de sections <spring>. On peut crer autant de groupes de sections que l'on veut dans [App.config]. un groupe de sections a des sections : c'est le cas ici : lignes 12-14 : la section <spring/context> lignes 15-22 : la section <spring/objects> lignes 4-9 : la rgion <configSections> dfinit la liste des gestionnaires (handlers) des groupes de sections prsents dans [App.config]. lignes 5-8 : dfinit la liste des gestionnaires des sections du groupe <spring> (name="spring"). ligne 6 : le gestionnaire de la section <context> du groupe <spring> : name : nom de la section gre type : nom de la classe grant la section sous la forme NomClasse, NomDLL.

Architectures 3 couches, tests unitaires NUnit, framework Spring

161

la section <context> du groupe <spring> est gre par la classe [Spring.Context.Support.ContextHandler] qui sera trouve dans la DLL [Spring.Core.dll] ligne 7 : le gestionnaire de la section <objects> du groupe <spring>

Les lignes 4-9 sont standard dans un fichier [App.config] avec Spring. On se contente de les recopier d'un projet l'autre.

lignes 12-14 : dfinit la section <spring/context>. ligne 13 : la balise <resource> a pour but d'indiquer o se trouve le fichier dfinissant les classes que Spring doit instancier. Celles-ci peuvent tre dans [App.config] comme ici mais elles peuvent tre galement dans un fichier de configuration autre. La localisation de ces classes est faite dans l'attribut uri de la balise <resource> : <resource uri="config://spring/objects> indique que la liste des classes instancier se trouve dans le fichier [App.config] (config:), dans la section //spring/objects, c.a.d. dans la balise <objects> de la balise <spring>. <resource uri="file://spring-config.xml"> indiquerait que la liste des classes instancier se trouve dans le fichier [spring-config.xml]. Celui-ci devrait tre plac dans les dossiers d'excution (bin/Release ou bin/Debug) du projet. Le plus simple est de le placer, comme il a t fait pour le fichier [DataImpot.txt], la racine du projet avec la proprit [Copy to output directory=always]. Les lignes 12-14 sont standard dans un fichier [App.config] avec Spring. On se contente de les recopier d'un projet l'autre. lignes 15-22 : dfinissent les classes instancier. C'est dans cette partie que se fait la configuration spcifique d'une application. La balise <objects> dlimite la section de dfinition des classes instancier. lignes 16-18 : dfinissent la classe instancier pour la couche [dao] ligne 16 : chaque objet instanci par Spring fait l'objet d'une balise <object>. Celle-ci a un attribut name qui est le nom de l'objet instanci. C'est via celui-ci que l'application demande Spring une rfrence : "donne-moi une rfrence sur l'objet qui s'appelle dao". L'attribut type dfinit la classe instancier sous la forme NomClasse, NomDLL. Ainsi la ligne 16 dfinit un objet appel "dao", instance de la classe "Dao.FileImpot" qui se trouve dans la DLL "ImpotsV5-dao.dll". On notera qu'on donne le nom complet de la classe (espace de noms inclus) et que le suffixe .dll n'est pas prcis dans le nom de la DLL. Une classe peut tre instancie de deux faons avec Spring : 1. via un constructeur particulier auquel on passe des paramtres : c'est ce qui est fait dans les lignes 16-18. 2. via le constructeur par dfaut sans paramtres. L'objet est alors initialis via ses proprits publiques : la balise <object> a alors des sous-balises <property> pour initialiser ces diffrentes proprits. Nous n'avons pas d'exemple de ce cas ici. ligne 16 : la classe instancie est la classe FileImpot. Celle-ci a le constructeur suivant :
public FileImpot(string fileName);

Les paramtres du constructeur sont dfinis l'aide de balises <constructor-arg>. ligne 17 : dfinit le 1er et seul paramtre du constructeur. L'attribut index est le n du paramtre du constructeur, l'attribut value sa valeur : <constructor-arg index="i" value="valuei"/> lignes 19-21 : dfinissent la classe instancier pour la couche [metier] : la classe [Metier.ImpotMetier] qui se trouve dans la DLL [ImpotsV5-metier.dll]. ligne 19 : la classe instancie est la classe ImpotMetier. Celle-ci a le constructeur suivant :

public ImpotMetier(IImpotDao dao);

ligne 20 : dfinit le 1er et seul paramtre du constructeur. Ci-dessus, le paramtre dao du constructeur est une rfrence d'objet. Dans ce cas, dans la balise <constructor-arg> on utilise l'attribut ref au lieu de l'attribut value qui a t utilis pour la couche [dao] : <constructor-arg index="i" ref="refi"/>. Dans le constructeur cidessus, le paramtre dao reprsente une instance sur la couche [dao]. Cette instance a t dfinie par les lignes 16-18 du fichier de configuration. Ainsi dans la ligne 20 :
<constructor-arg index="0" ref="dao"/>

ref="dao" reprsente l'objet Spring "dao" dfini par les lignes 16-18. Pour rsumer, le fichier [App.config] :

instancie la couche [dao] avec la classe FileImpot qui reoit pour paramtre DataImpot.txt (ligne 16-18). L'objet rsultant est appel "dao" instancie la couche [metier] avec la classe ImpotMetier qui reoit pour paramtre l'objet "dao" prcdent (lignes 19-21).

Architectures 3 couches, tests unitaires NUnit, framework Spring

162

Il ne nous reste plus qu' utiliser ce fichier de configuration Spring dans la couche [ui]. Pour cela, nous dupliquons la classe [Dialogue.cs] en [Dialogue2.cs] et nous faisons de cette dernire la classe principale du projet [ui] : 4

2 1

en [1] : copie de [Dialogue.cs] en [2] : collage en [3] : la copie de [Dialogue.cs] en [4] : renomme [Dialogue2.cs]

en [6] : on fait de [Dialogue2.cs] la classe principale du projet [ui].

Le code suivant de [Dialogue.cs] :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. // on cre les couches [metier et dao] IImpotMetier metier = null; try { // cration couche [metier] metier = new ImpotMetier(new FileImpot("DataImpot.txt")); } catch (ImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); // arrt programme Environment.Exit(1); } // boucle infinie while (true) { ...

devient le suivant dans [Dialogue2.cs] :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. // on cre les couches [metier et dao] IApplicationContext ctx = null; try { // contexte Spring ctx = ContextRegistry.GetContext(); } catch (Exception e1) { // affichage erreur Console.WriteLine("Chane des exceptions : \n{0}", "".PadLeft(40, '-')); Exception e = e1; while (e != null) {

Architectures 3 couches, tests unitaires NUnit, framework Spring

163

11. Console.WriteLine("{0}: {1}", e.GetType().FullName, e.Message); 12. Console.WriteLine("".PadLeft(40, '-')); 13. e = e.InnerException; 14. } 15. // arrt programme 16. Environment.Exit(1); 17. } 18. // on demande une rfrence sur la couche [metier] 19. IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier"); 20. // boucle infinie 21. while (true) { 22. ....................................

ligne 2 : IApplicationContext donne accs l'ensemble des objets instancis par Spring. On appelle cet objet, le contexte Spring de l'application ou plus simplement le contexte de l'application. Pour l'instant, ce contexte n'a pas t initialis. C'est le try / catch qui suit qui le fait. ligne 5 : la configuration de Spring dans [App.config] est lue et exploite. Aprs cette opration, s'il n'y a pas eu d'exception, tous les objets de la section <objects> ont t instancis : l'objet Spring "dao" est une instance sur la couche [dao] l'objet Spring "metier" est une instance sur la couche [metier] ligne 19 : la classe [Dialogue2.cs] a besoin d'une rfrence sur la couche [metier]. Celle-ci est demande au contexte de l'application. L'objet IApplicationContext donne accs aux objets Spring via leur nom (attribut name de la balise <object> de la configuration Spring). La rfrence rendue est une rfrence sur le type gnrique Object. On est amens transtyper la rfrence rendue dans le bon type, ici le type de l'interface de la couche [metier] : IImpotMetier. Si tout s'est bien pass, aprs la ligne 19, [Dialogue2.cs] a une rfrence sur la couche [metier]. Le code des lignes 21 et audel est celui de la classe [Dialogue.cs] dj tudie. lignes 6-17 : gestion de l'exception qui survient lorsque l'exploitation du fichier de configuration de Spring ne peut tre mene son terme. Il peut y avoir diverses raisons cela : syntaxe incorrecte du fichier de configuration lui-mme ou bien impossibilit instancier l'un des objets configurs. Dans notre exemple, ce dernier cas se produirait si le fichier DataImpot.txt de la ligne 17 de [App.config] n'tait pas trouv dans le dossier d'excution du projet. L'exception qui remonte ligne 6 est une chane d'exceptions o chaque exception a deux proprits : Message : le message d'erreur lie l'exception InnerException : l'exception prcdente dans la chane des exceptions La boucle des lignes 10-14 fait afficher toutes les exceptions de la chane sous la forme : classe de l'exception et message associ.

Lorsqu'on excute le projet [ui] avec un fichier de configuration valide, on obtient les rsultats habituels :
Paramtres du calcul de l'Impot au format : Mari (o/n) NbEnfants Salaire ou rien pour arrter :o 2 60000 Impot=4282 euros

Lorsqu'on excute le projet [ui] avec un fichier [DataImpotInexistant.txt] inexistant,


<object name="dao" type="Dao.FileImpot, ImpotsV5-dao"> <constructor-arg index="0" value="DataImpotInexistant.txt"/> </object>

on obtient les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. Chane des exceptions : ---------------------------------------System.Configuration.ConfigurationErrorsException: Error creating context 'spring.root': Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'. ---------------------------------------Spring.Util.FatalReflectionException: Cannot instantiate Type [Spring.Context.Support.XmlApplicationContext] using ctor [Void .ctor(System.String, Boolean, System.String[])] : 'Exception has been thrown by the target of an invocation.' ---------------------------------------System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---------------------------------------Spring.Objects.Factory.ObjectCreationException: Error creating object with name'dao' defined in 'config [spring/objects]' : Initialization of object failed : Cannot instantiate Type

Architectures 3 couches, tests unitaires NUnit, framework Spring

164

10. 11. 12. 13. 14. 15. 16. 17.

[Dao.FileImpot] using ctor [Void .ctor(System.String)] :'Exception has been thrown by the target of an invocation.' ---------------------------------------Spring.Util.FatalReflectionException: Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] : 'Exception has been thrown by the targetof an invocation.' ---------------------------------------System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---------------------------------------Entites.ImpotException: Erreur lors de la lecture du fichier DataImpotInexistant.txt ---------------------------------------System.IO.FileNotFoundException: Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.

ligne 17 : l'exception originelle de type [FileNotFoundException] ligne 15 : la couche [dao] encapsule cette exception dans un type [Entites.ImpotException] ligne 9 : l'exception lance par Spring parce qu'il n'a pas russi instancier l'objet nomm "dao". Dans le processus de cration de cet objet, deux autres exceptions sont intervenues auparavant : celles des lignes 11 et 13. parce que l'objet "dao" n'a pu tre cr, le contexte de l'application n'a pu tre cr. C'est le sens de l'exception ligne 5. Auparavant, une autre exception, celle de la ligne 7 s'tait produite. ligne 3 : l'exception de plus haut niveau, la dernire de la chane : une erreur de configuration est signale.

De tout cela, on retiendra que c'est l'exception la plus profonde, ici celle de la ligne 17 qui est souvent la plus significative. On notera cependant que Spring a conserv le message d'erreur de la ligne 17 pour le remonter l'exception de plus haut niveau ligne 3 afin d'avoir la cause originelle de l'erreur au niveau le plus haut. Spring mrite lui tout seul un livre. Nous n'avons fait ici qu'effleurer le sujet. On pourra l'approfondir avec le document [springnet-reference.pdf] qu'on trouve dans le dossier d'installation de Spring :

On pourra lire galement [http://tahe.developpez.com/dotnet/springioc], un tutoriel Spring prsent dans un contexte VB.NET.

Architectures 3 couches, tests unitaires NUnit, framework Spring

165

5
5.1

Interfaces graphiques
Les bases des interfaces graphiques

Nous nous proposons ici de donner les premiers lments pour construire des interfaces graphiques et grer leurs vnements.

5.1.1

Un premier projet

Construisons un premier projet de type "Application windows" :

4 1 2

[1] : crer un nouveau projet [2] : de type Application Windows [3] : le nom du projet importe peu pour le moment [4] : le projet cr

6 7 8 9

[5] : on sauvegarde la solution courante [6] : nom du projet [7] : dossier de la solution [8] : nom de la solution [9] : un dossier sera cr pour la solution [Chap5]. Les projets de celle-ci seront dans des sous-dossiers.

Interfaces graphiques

166

11 12

10

[10] : le projet [01] dans la solution [Chap5] : [Program.cs] est la classe principale du projet [Form1.cs] est le fichier source qui va grer le comportement de la fentre [11] [Form1.Designer.cs] est le fichier source qui va encapsuler l'information sur les composants de la fentre [11] [11] : le fichier [Form1.cs] en mode "conception" (design) [12] : l'application gnre peut tre excute par (Ctrl-F5). La fentre [Form1] s'affiche. On peut la dplacer, la redimensionner et la fermer. On a donc les lments de base d'une fentre graphique.

La classe principale [Program.cs] est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using System; using System.Windows.Forms; namespace Chap5 { static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }

ligne 2 : les applications avec formulaires utilisent l'espace de noms System.Windows.Forms. ligne 4 : l'espace de noms initial a t renomm en Chap5. ligne 10 : l'excution du projet (Ctrl-F5), la mthode [Main] est excute. lignes 11-13 : la classe Application appartient l'espace de noms System.Windows.Forms. Elle contient des mthodes statiques pour lancer / arrter les applications graphiques windows. ligne 11 : facultative - permet de donner diffrents styles visuels aux contrles dposs sur un formulaire ligne 12 : facultative - fixe le moteur de rendu des textes des contrles : GDI+ (true), GDI (false) ligne 13 : la seule ligne indispensable de la mthode [Main] : instancie la classe [Form1] qui est la classe du formulaire et lui demande de s'excuter.

Le fichier source [Form1.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using System; using System.Windows.Forms; namespace Chap5 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } }

ligne 5 : la classe Form1 drive de la classe [System.Windows.Forms.Form] qui est la classe mre de toutes les fentres. Le mot cl partial indique que la classe est partielle et qu'elle peut tre complte par d'autres fichiers source. C'est le cas ici, o la classe Form1 est rpartie dans deux fichiers : [Form1.cs] : dans lequel on trouvera le comportement du formulaire, notamment ses gestionnaires d'vnements

Interfaces graphiques

167

[Form1.Designer.cs] : dans lequel on trouvera les composants du formulaire et leurs proprits. Ce fichier a la particularit d'tre rgnr chaque fois que l'utilisateur modifie la fentre en mode [conception]. lignes 6-8 : le constructeur de la classe Form1 ligne 7 : fait appel la mthode InitializeComponent. On voit que cette mthode n'est pas prsente dans [Form1.cs]. On la trouve dans [Form1.Designer.cs].

Le fichier source [Form1.Designer.cs] est le suivant :


1. namespace Chap5 { 2. partial class Form1 { 3. /// <summary> 4. /// Required designer variable. 5. /// </summary> 6. private System.ComponentModel.IContainer components = null; 7. 8. /// <summary> 9. /// Clean up any resources being used. 10. /// </summary> 11. /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param> 12. protected override void Dispose(bool disposing) { 13. if (disposing && (components != null)) { 14. components.Dispose(); 15. } 16. base.Dispose(disposing); 17. } 18. 19. #region Windows Form Designer generated code 20. 21. /// <summary> 22. /// Required method for Designer support - do not modify 23. /// the contents of this method with the code editor. 24. /// </summary> 25. private void InitializeComponent() { 26. this.SuspendLayout(); 27. // 28. // Form1 29. // 30. this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 31. this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 32. this.ClientSize = new System.Drawing.Size(196, 98); 33. this.Name = "Form1"; 34. this.Text = "Form1"; 35. this.ResumeLayout(false); 36. 37. } 38. 39. #endregion 40. 41. } 42. }

ligne 2 : il s'agit toujours de la classe Form1. On notera qu'il n'est plus besoin de rpter qu'elle drive de la classe Form. lignes 25-37 : la mthode InitializeComponent appele par le constructeur de la classe [Form1]. Cette mthode va crer et initialiser tous les composants du formulaire. Elle est rgnre chaque changement de celui-ci en mode [conception]. Une section, appele rgion, est cre pour la dlimiter lignes 19-39. Le dveloppeur ne doit pas ajouter de code dans cette rgion : il sera cras la rgnration suivante.

Il est plus simple dans un premier temps de ne pas s'intresser au code de [Form1.Designer.cs]. Il est gnr automatiquement et est la traduction en langage C# des choix que le dveloppeur fait en mode [conception]. Prenons un premier exemple :

Interfaces graphiques

168

3 5 4

[1] : slectionner le mode [conception] en double-cliquant sur le fichier [Form1.cs] [2] : cliquer droit sur le formulaire et choisir [Properties] [3] : la fentre des proprits de [Form1] [4] : la proprit [Text] reprsente le titre de la fentre [5] : le changement de la proprit [Text] est pris en compte en mode [conception] ainsi que dans le code source [Form1.Designer.cs] :
private void InitializeComponent() { this.SuspendLayout(); ... ... } this.Text = "Mon 1er formulaire";

1. 2. 3. 4. 5. 6.

5.1.2
5.1.2.1

Un second projet
Le formulaire

Nous commenons un nouveau projet appel 02. Pour cela nous suivons la procdure explicite prcdemment pour crer un projet. La fentre crer est la suivante :

1 3

Les composants du formulaire sont les suivants : n nom type rle 1 labelSaisie Label un libell 2 textBoxSaisie TextBox une zone de saisie 3 buttonAfficher Button pour afficher dans une bote de dialogue le contenu de la zone de saisie textBoxSaisie On pourra procder comme suit pour construire cette fentre :

Interfaces graphiques

169

1 2

[1] : cliquer droit sur le formulaire en-dehors de tout composant et choisir l'option [Properties] [2] : la feuille de proprits de la fentre apparat dans le coin infrieur droit de Visual studio

Parmi les proprits du formulaire noter :


BackColor ForeColor Menu Text FormBorderStyle Font Name

pour fixer la couleur de fond de la fentre pour fixer la couleur des dessins ou du texte sur la fentre pour associer un menu la fentre pour donner un titre la fentre pour fixer le type de fentre pour fixer la police de caractres des critures dans la fentre pour fixer le nom de la fentre

Ici, nous fixons les proprits Text et Name :


Text Name

Saisies et boutons - 1 frmSaisiesBoutons

3 2 1

4 5

[1] : choisir la bote outils [Common Controls] parmi les botes outils proposes par Visual Studio [2, 3, 4] : double-cliquer successivement sur les composants [Label], [Button] et [TextBox] [5] : les trois composants sont sur le formulaire

Pour aligner et dimensionner correctement les composants, on peut utiliser les lments de la barre d'outils :

Interfaces graphiques

170

Le principe du formatage est le suivant : 1. slectionnez les diffrents composants formater ensemble (touche Ctrl appuye pendant les diffrents clics slectionnant les composants) 2. slectionnez le type de formatage dsir : les options Align permettent d'aligner des composants par le haut, le bas, le ct gauche ou droit, le milieu les options Make Same Size permettent que des composants aient la mme hauteur ou la mme largeur l'option Horizontal Spacing permet d'aligner horizontalement des composants avec des intervalles entre eux de mme largeur. Idem pour l'option Vertical Spacing pour aligner verticalement. l'option Center permet de centrer un composant horizontalement (Horizontally) ou verticalement (Vertically) dans la fentre Une fois placs les composants nous fixons leurs proprits. Pour cela, cliquer droit sur le composant et prendre l'option Properties :

4 2 5

1 3

[1] : slectionner le composant pour avoir sa fentre de proprits. Dans celle-ci, modifier les proprits suivantes : name : labelSaisie, text : Saisie [2] : procder de mme : name : textBoxSaisie, text : ne rien mettre [3] : name : buttonAfficher, text : Afficher [4] : la fentre elle-mme : name : frmSaisiesBoutons, text : Saisies et boutons - 1 [5] : excuter (Ctrl-F5) le projet pour avoir un premier aperu de la fentre en action.

Ce qui a t fait en mode [conception] a t traduit dans le code de [Form1.Designer.cs] :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. namespace Chap5 { partial class frmSaisiesBoutons { ... private System.ComponentModel.IContainer components = null; ... private void InitializeComponent() { this.labelSaisie = new System.Windows.Forms.Label(); this.buttonAfficher = new System.Windows.Forms.Button(); this.textBoxSaisie = new System.Windows.Forms.TextBox(); this.SuspendLayout(); // // labelSaisie // this.labelSaisie.AutoSize = true; this.labelSaisie.Location = new System.Drawing.Point(12, 19); this.labelSaisie.Name = "labelSaisie";

Interfaces graphiques

171

17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. } 58. }

this.labelSaisie.Size = new System.Drawing.Size(35, 13); this.labelSaisie.TabIndex = 0; this.labelSaisie.Text = "Saisie"; // // buttonAfficher // this.buttonAfficher.Location = new System.Drawing.Point(80, 49); this.buttonAfficher.Name = "buttonAfficher"; this.buttonAfficher.Size = new System.Drawing.Size(75, 23); this.buttonAfficher.TabIndex = 1; this.buttonAfficher.Text = "Afficher"; this.buttonAfficher.UseVisualStyleBackColor = true; this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click); // // textBoxSaisie // this.textBoxSaisie.Location = new System.Drawing.Point(80, 19); this.textBoxSaisie.Name = "textBoxSaisie"; this.textBoxSaisie.Size = new System.Drawing.Size(100, 20); this.textBoxSaisie.TabIndex = 2; // // frmSaisiesBoutons // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(292, 118); this.Controls.Add(this.textBoxSaisie); this.Controls.Add(this.buttonAfficher); this.Controls.Add(this.labelSaisie); this.Name = "frmSaisiesBoutons"; this.Text = "Saisies et boutons - 1"; this.ResumeLayout(false); this.PerformLayout(); } private System.Windows.Forms.Label labelSaisie; private System.Windows.Forms.Button buttonAfficher; private System.Windows.Forms.TextBox textBoxSaisie;

lignes 53-55 : les trois composants ont donn naissance trois champs privs de la classe [Form1]. On notera que les noms de ces champs sont les noms donns aux composants en mode [conception]. C'est le cas galement du formulaire ligne 2 qui est la classe elle-mme. lignes 7-9 : les trois objets de type [Label], [TextBox] et [Button] sont crs. C'est travers eux que les composants visuels sont grs. lignes 14-19 : configuration du label labelSaisie lignes 23-29 : configuration du bouton buttonAfficher lignes 33-36 : configuration du champ de saisie textBoxSaisie lignes 40-47 : configuration du formulaire frmSaisiesBoutons. On notera, lignes 43-45, la faon d'ajouter des composants au formulaire.

Ce code est comprhensible. Il est ainsi possible de construire des formulaires par code sans utiliser le mode [conception]. De nombreux exemples de ceci sont donns dans la documentation MSDN de Visual Studio. Matriser ce code permet de crer des formulaires en cours d'excution : par exemple, crer la vole un formulaire permettant la mise jour d'une table de base de donnes, la structure de cette table n'tant dcouverte qu' l'excution. Il nous reste crire la procdure de gestion d'un clic sur le bouton Afficher. Slectionner le bouton pour avoir accs sa fentre de proprits. Celle-ci a plusieurs onglets :

Interfaces graphiques

172

[1] : liste des proprits par ordre alphabtique [2] : vnements lis au contrle

Les proprits et vnements d'un contrle sont accessibles par catgories ou par ordre alphabtique :

[3] : Proprits ou vnements par catgorie [4] : Proprits ou vnements par ordre alphabtique

L'onglet Events en mode Catgories pour le bouton buttonAfficher est le suivant :

3 1 2

[1] : la colonne de gauche de la fentre liste les vnements possibles sur le bouton. Un clic sur un bouton correspond l'vnement Click. [2] : la colonne de droite contient le nom de la procdure appele lorsque l'vnement correspondant se produit. [3] : si on double-clique sur la cellule de l'vnement Click, on passe alors automatiquement dans la fentre de code pour crire le gestionnaire de l'vnement Click sur le bouton buttonAfficher :
using System; using System.Windows.Forms; namespace Chap5 { public partial class frmSaisiesBoutons : Form { public frmSaisiesBoutons() { InitializeComponent(); } private void buttonAfficher_Click(object sender, EventArgs e) { } } }

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14.

lignes 10-12 : le squelette du gestionnaire de l'vnement Click sur le bouton nomm buttonAfficher. On notera les points suivants : la mthode est nomme selon le schma nomDuComposant_NomEvnement la mthode est prive. Elle reoit deux paramtres : sender : est l'objet qui a provoqu l'vnement. Si la procdure est excute la suite d'un clic sur le bouton buttonAfficher, sender sera gal buttonAfficher. On peut imaginer que la procdure buttonAfficher_Click soit excute partir d'une autre procdure. Celle-ci aurait alors tout loisir de mettre comme premier paramtre, l'objet sender de son choix. EventArgs : un objet qui contient des informations sur l'vnement. Pour un vnement Click, il ne contient rien. Pour un vnement ayant trait aux dplacements de la souris, on y trouvera les coordonnes (X,Y) de la souris. nous n'utiliserons aucun de ces paramtres ici.

Ecrire un gestionnaire d'vnement consiste complter le squelette de code prcdent. Ici, nous voulons prsenter une bote de dialogue avec dedans, le contenu du champ textBoxSaisie s'il est non vide [1], un message d'erreur sinon [2] :

Interfaces graphiques

173

Le code ralisant cela pourrait-tre le suivant :


1. 2. 3. 4. 5. 6. 7. 8. private void buttonAfficher_Click(object sender, EventArgs e) { // on affiche le texte qui a t saisi dans le TextBox textboxSaisie string texte = textBoxSaisie.Text.Trim(); if (texte.Length != 0) { MessageBox.Show("Texte saisi= " + texte, "Vrification de la saisie", MessageBoxButtons.OK, MessageBoxIcon.Information); } else { MessageBox.Show("Saissez un texte...", "Vrification de la saisie", MessageBoxButtons.OK, MessageBoxIcon.Error); }

La classe MessageBox sert afficher des messages dans une fentre. Nous avons utilis ici la mthode Show suivante :
public static DialogResult Show(string text, string caption, MessageBoxButtons buttons, MessageBoxIcon icon);

avec
text caption buttons icon

le message afficher le titre de la fentre les boutons prsents dans la fentre l'icone prsente dans la fentre

Le paramtre buttons peut prendre ses valeurs parmi les constantes suivantes (prfixes par MessageBoxButtons comme montr ligne 7) ci-dessus :
constante

boutons

AbortRetryIgnore

OK

OKCancel

Interfaces graphiques

174

constante

boutons

RetryCancel

YesNo

YesNoCancel

Le paramtre icon peut prendre ses valeurs parmi les constantes suivantes (prfixes par MessageBoxIcon comme montr ligne 10) cidessus :
Asterisk Error

idem Stop

Exclamation

idem Warning

Hand

Information

idem Asterisk

None

Question

Stop

idem Hand

Warning

Interfaces graphiques

175

La mthode Show est une mthode statique qui rend un rsultat de type [System.Windows.Forms.DialogResult] qui est une numration :

Pour savoir sur quel bouton a appuy l'utilisateur pour fermer la fentre de type MessageBox on crira :
1. 2. DialogResult res=MessageBox.Show(..); if (res==DialogResult.Yes){ // il a appuy sur le bouton oui...}

5.1.2.2

Le code li la gestion des vnements

Outre la fonction buttonAfficher_Click que nous avons crite, Visual studio a gnr dans la mthode InitializeComponents de [Form1.Designer.cs] qui cre et initialise les composants du formulaire, la ligne suivante :
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

Click est un vnement de la classe Button [1, 2, 3] : 1 4

2 5

Interfaces graphiques

176

[5] : la dclaration de l'vnement [Control.Click] [4]. Ainsi on voit que l'vnement Click n'est pas propre la classe [Button]. Il appartient la classe [Control], classe parente de la classe [Button]. EventHandler est un prototype (un modle ) de mthode appel delegate. Nous y allons y revenir. event est un mot cl qui restreint les fonctionnalits du delegate EventHandler : un objet delegate a des fonctionnalits plus riches qu'un objet event.

Le delegate EventHandler est dfini comme suit :

Le delegate EventHandler dsigne un modle de mthode : ayant pour 1er paramtre un type Object ayant pour 2ime paramtre un type EventArgs ne rendant aucun rsultat C'est le cas de la mthode de gestion du clic sur le bouton buttonAfficher qui a t gnre par Visual Studio :
private void buttonAfficher_Click(object sender, EventArgs e);

Ainsi la mthode buttonAfficher_Click correspond au prototype dfini par le type EventHandler. Pour crer un objet de type EventHandler, on procde comme suit :
EventHandler evtHandler=new EventHandler(mthode correspondant au prototype EventHandler); dfini par le type

Puisque la mthode buttonAfficher_Click correspond au prototype dfini par le type EventHandler, on pourra crire :
EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Une variable de type delegate est en fait une liste de rfrences sur des mthodes du type du delegate. Pour ajouter une nouvelle mthode M la variable evtHandler ci-dessus, on utilisera la syntaxe :
evtHandler+=new EvtHandler(M);

La notation += peut tre utilise mme si evtHandler est une liste vide. Revenons la ligne de [InitializeComponent] qui ajoute un gestionnaire d'vnement l'vnement Click de l'objet buttonAfficher :
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

Cette instruction ajoute une mthode de type EventHandler la liste des mthodes du champ buttonAfficher.Click. Ces mthodes seront appeles chaque fois que l'vnement Click sur le composant buttonAfficher sera dtect. Il n'y en a souvent qu'une. On l'appelle le "gestionnaire de l'vnement". Revenons sur la signature de EventHandler :
private delegate void EventHandler(object sender, EventArgs e);

Interfaces graphiques

177

Le second paramtre du delegate est un objet de type EventArgs ou d'une classe drive. Le type EventArgs est trs gnral et n'apporte en fait aucune information sur l'vnement qui s'est produit. Pour un clic sur un bouton, c'est suffisant. Pour un dplacement de souris sur un formulaire, on aurait un vnement MouseMove de la classe [Form] dfini par :
public event MouseEventHandler MouseMove;

Le delegate MouseEventHandler est dfini comme :

C'est une fonction dlgue (delegate) de signature void f (object, MouseEventArgs). La classe MouseEventArgs est elle dfinie par :

La classe MouseEventArgs est plus riche que la classe EventArgs. On peut par exemple connatre les coordonnes de la souris X et Y au moment o se produit l'vnement.

5.1.2.3

Conclusion

Des deux projets tudis, nous pouvons conclure qu'une fois l'interface graphique construite avec Visual studio, le travail du dveloppeur consiste principalement crire les gestionnaires des vnements qu'il veut grer pour cette interface graphique. Du code est gnr automatiquement par Visual Studio. Ce code, qui peut tre complexe, peut tre ignor en premire approche. Ultrieurement, son tude peut permettre une meilleure comprhension de la cration et de la gestion des formulaires.

5.2

Les composants de base

Nous prsentons maintenant diverses applications mettant en jeu les composants les plus courants afin de dcouvrir les principales mthodes et proprits de ceux-ci. Pour chaque application, nous prsentons l'interface graphique et le code intressant, principalement celui des gestionnaires d'vnements.

5.2.1

Formulaire Form

Nous commenons par prsenter le composant indispensable, le formulaire sur lequel on dpose des composants. Nous avons dj prsent quelques-unes de ses proprits de base. Nous nous attardons ici sur quelques vnements importants d'un formulaire.
Load

le formulaire est en cours de chargement

Interfaces graphiques

178

Closing Closed

le formulaire est en cours de fermeture le formulaire est ferm

L'vnement Load se produit avant mme que le formulaire ne soit affich. L'vnement Closing se produit lorsque le formulaire est en cours de fermeture. On peut encore arrter cette fermeture par programmation. Nous construisons un formulaire de nom Form1 sans composant :

1 2

[1] : le formulaire [2] : les trois vnements traits

Le code de [Form1.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System; using System.Windows.Forms; namespace Chap5 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { // chargement initial du formulaire MessageBox.Show("Evt Load", "Load"); }

private void Form1_FormClosing(object sender, FormClosingEventArgs e) { // le formulaire est en train de se fermer MessageBox.Show("Evt FormClosing", "FormClosing"); // on demande confirmation DialogResult rponse = MessageBox.Show("Voulez-vous vraiment quitter l'application", "Closing", MessageBoxButtons.YesNo, MessageBoxIcon.Question); 20. if (rponse == DialogResult.No) 21. e.Cancel = true; 22. } 23. 24. private void Form1_FormClosed(object sender, FormClosedEventArgs e) { 25. // le formulaire va tre ferm 26. MessageBox.Show("Evt FormClosed", "FormClosed"); 27. } 28. } 29. }

Nous utilisons la fonction MessageBox pour tre averti des diffrents vnements.

Interfaces graphiques

179

ligne 10 : L'vnement Load va se produire au dmarrage de l'application avant mme que le formulaire ne s'affiche :

ligne 15 : L'vnement FormClosing va se produire lorsque l'utilisateur ferme la fentre.

ligne 19 : Nous lui demandons alors s'il veut vraiment quitter l'application :

ligne 20 : S'il rpond Non, nous fixons la proprit Cancel de l'vnement CancelEventArgs e que la mthode a reu en paramtre. Si nous mettons cette proprit False, la fermeture de la fentre est abandonne, sinon elle se poursuit. L'vnement FormClosed va alors se produire :

5.2.2

Etiquettes Label et botes de saisie TextBox

Nous avons dj rencontr ces deux composants. Label est un composant texte et TextBox un composant champ de saisie. Leur proprit principale est Text qui dsigne soit le contenu du champ de saisie soit le texte du libell. Cette proprit est en lecture/criture. L'vnement habituellement utilis pour TextBox est TextChanged qui signale que l'utilisateur modifi le champ de saisie. Voici un exemple qui utilise l'vnement TextChanged pour suivre les volutions d'un champ de saisie :

1 2 3 4

n 1 2 3 4

type TextBox Label Button Button

nom textBoxSaisie labelControle buttonEffacer buttonQuitter

rle champ de saisie affiche le texte de 1 en temps rel AutoSize=False, Text=(rien) pour effacer les champs 1 et 2 pour quitter l'application

Le code de cette application est le suivant :


1. using System.Windows.Forms;

Interfaces graphiques

180

2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29.

namespace Chap5 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void textBoxSaisie_TextChanged(object sender, System.EventArgs e) { // le contenu du TextBox a chang - on le copie dans le Label labelControle labelControle.Text = textBoxSaisie.Text; } private void buttonEffacer_Click(object sender, System.EventArgs e) { // on efface le contenu de la bote de saisie textBoxSaisie.Text = ""; } private void buttonQuitter_Click(object sender, System.EventArgs e) { // clic sur bouton Quitter - on quitte l'application Application.Exit(); } private void Form1_Shown(object sender, System.EventArgs e) { // on met le focus sur le champ de saisie textBoxSaisie.Focus(); }

} }

ligne 24 : l'vnement [Form].Shown a lieu lorsque le formulaire est affich ligne 26 : on met alors le focus (pour une saisie) sur le composant textBoxSaisie. ligne 9 : l'vnement [TextBox].TextChanged se produit chaque fois que le contenu d'un composant TextBox change ligne 11 : on recopie le contenu du composant [TextBox] dans le composant [Label] ligne 14 : gre le clic sur le bouton [Effacer] ligne 16 : on met la chane vide dans le composant [TextBox] ligne 19 : gre le clic sur le bouton [Quitter] ligne 21 : pour arrter l'application en cours d'excution. On se rappelle que l'objet Application sert lancer l'application dans la mthode [Main] de [Form1.cs] :
static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); }

1. 2. 3. 4. 5.

L'exemple suivant utilise un TextBox multilignes :

3 2

La liste des contrles est la suivante : n 1 2 type TextBox TextBox nom textBoxLignes textBoxLigne rle champ de saisie multilignes Multiline=true, ScrollBars=Both, AcceptReturn=True, AcceptTab=True champ de saisie monoligne

Interfaces graphiques

181

Button

buttonAjouter

Ajoute le contenu de 2 1

Pour qu'un TextBox devienne multilignes on positionne les proprits suivantes du contrle :
Multiline=true ScrollBars=( None, Horizontal, Vertical, Both) AcceptReturn=(True, False) AcceptTab=(True, False)

pour accepter plusieurs lignes de texte pour demander ce que le contrle ait des barres de dfilement (Horizontal, Vertical, Both) ou non (None) si gal true, la touche Entre fera passer la ligne si gal true, la touche Tab gnrera une tabulation dans le texte

L'application permet de taper des lignes directement dans [1] ou d'en ajouter via [2] et [3]. Le code de l'application est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. using System.Windows.Forms; using System; namespace Chap5 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void buttonAjouter_Click(object sender, System.EventArgs e) { // ajout du contenu de textBoxLigne celui de textBoxLignes textBoxLignes.Text += textBoxLigne.Text+Environment.NewLine; textBoxLigne.Text = ""; } private void Form1_Shown(object sender, EventArgs e) { // on met le focus sur le champ de saisie textBoxLigne.Focus(); } } }

ligne 18 : lorsque le formulaire est affich (vt Shown), on met le focus sur le champ de saisie textBoxLigne ligne 10 : gre le clic sur le bouton [Ajouter] ligne 12 : le texte du champ de saisie textBoxLigne est ajout au texte du champ de saisie textBoxLignes suivi d'un saut de ligne. ligne 13 : le champ de saisie textBoxLigne est effac

5.2.3

Listes droulantes ComboBox

Nous crons le formulaire suivant :

type ComboBox

nom comboNombres

rle contient des chanes de caractres DropDownStyle=DropDownList

Un composant ComboBox est une liste droulante double d'une zone de saisie : l'utilisateur peut soit choisir un lment dans (2) soit taper du texte dans (1). Il existe trois sortes de ComboBox fixes par la proprit DropDownStyle :
Simple

liste non droulante avec zone d'dition

Interfaces graphiques

182

DropDown DropDownList

liste droulante avec zone d'dition liste droulante sans zone d'dition

Par dfaut, le type d'un ComboBox est DropDown. La classe ComboBox a un seul constructeur :
new ComboBox()

cre un combo vide

Les lments du ComboBox sont disponibles dans la proprit Items :


public ComboBox.ObjectCollection Items {get;}

C'est une proprit indexe, Items[i] dsignant l'lment i du Combo. Elle est en lecture seule. Soit C un combo et C.Items sa liste d'lments. On a les proprits suivantes :
C.Items.Count C.Items[i] C.Add(object o) C.AddRange(object[] objets) C.Insert(int i, object o) C.RemoveAt(int i) C.Remove(object o) C.Clear() C.IndexOf(object o) C.SelectedIndex C.SelectedItem C.SelectedItem.Text C.Text

nombre d'lments du combo lment i du combo ajoute l'objet o en dernier lment du combo ajoute un tableau d'objets en fin de combo ajoute l'objet o en position i du combo enlve l'lment i du combo enlve l'objet o du combo supprime tous les lments du combo rend la position i de l'objet o dans le combo index de l'lment slectionn lment slectionn texte affich de l'lment slectionn texte affich de l'lment slectionn

On peut s'tonner qu'un combo puisse contenir des objets alors que visuellement il affiche des chanes de caractres. Si un ComboBox contient un objet obj, il affiche la chane obj.ToString(). On se rappelle que tout objet a une mthode ToString hrite de la classe object et qui rend une chane de caractres "reprsentative" de l'objet. L'lment Item slectionn dans le combo C est C.SelectedItem ou C.Items[C.SelectedIndex] o C.SelectedIndex est le n de l'lment slectionn, ce n partant de zro pour le premier lment. Le texte slectionn peut tre obtenu de diverses faons : C.SelectedItem.Text, C.Text Lors du choix d'un lment dans la liste droulante se produit l'vnement SelectedIndexChanged qui peut tre alors utilis pour tre averti du changement de slection dans le combo. Dans l'application suivante, nous utilisons cet vnement pour afficher l'lment qui a t slectionn dans la liste.

Le code de l'application est le suivant :


1. 2. using System.Windows.Forms;

Interfaces graphiques

183

3. namespace Chap5 { 4. public partial class Form1 : Form { 5. private int previousSelectedIndex=0; 6. 7. public Form1() { 8. InitializeComponent(); 9. // remplissage combo 10. comboBoxNombres.Items.AddRange(new string[] { "zro", "un", "deux", "trois", "quatre" }); 11. // slection lment n 0 12. comboBoxNombres.SelectedIndex = 0; 13. } 14. 15. private void comboBoxNombres_SelectedIndexChanged(object sender, System.EventArgs e) { 16. int newSelectedIndex = comboBoxNombres.SelectedIndex; 17. if (newSelectedIndex != previousSelectedIndex) { 18. // l'lment slectionn chang - on l'affiche 19. MessageBox.Show(string.Format("Elment slectionn : ({0},{1})", comboBoxNombres.Text, newSelectedIndex), "Combo", MessageBoxButtons.OK, MessageBoxIcon.Information); 20. // on note le nouvel index 21. previousSelectedIndex = newSelectedIndex; 22. } 23. } 24. } 25. }

ligne 5 : previousSelectedIndex mmorise le dernier index slectionn dans le combo ligne 10 : remplissage du combo avec un tableau de chanes de caractres ligne 12 : le 1er lment est slectionn ligne 15 : la mthode excute chaque fois que l'utilisateur slectionne un lment du combo. Contrairement ce que pourrait laisser croire le nom de l'vnement, celui-ci a lieu mme si l'lment slectionn est le mme que le prcdent. ligne 16 : on note l'index de l'lment slectionn ligne 17 : s'il est diffrent du prcdent ligne 19 : on affiche le n et le texte de l'lment slectionn ligne 21 : on note le nouvel index

5.2.4

Composant ListBox

On se propose de construire l'interface suivante :

0 1 5 3 7 6 4 2

Les composants de cette fentre sont les suivants : n 0 1 type Form TextBox nom Form1 textBoxSaisie rle/proprits formulaire FormBorderStyle=FixedSingle (cadre non redimensionable) champ de saisie

Interfaces graphiques

184

2 3 4 5 6 7 8

Button ListBox ListBox Button Button Button Button

buttonAjouter listBox1 listBox2 button1vers2 button2vers1 buttonEffacer1 buttonEffacer2

bouton permettant d'ajouter le contenu du champ de saisie [1] dans la liste [3] liste 1 SelectionMode=MultiExtended : liste 2 SelectionMode=MultiSimple : transfre les lments slectionns de liste 1 vers liste 2 fait l'inverse vide la liste 1 vide la liste 2

Les composants ListBox ont un mode de slection de leurs lments qui est dfini par leur proprit SelectionMode :
One MultiExtended MultiSimple

un seul lment peut tre slectionn multi-slection possible : maintenir appuye la touche SHIFT et cliquer sur un lment tend la slection de l'lment prcdemment slectionn l'lment courant. multi-slection possible : un lment est slectionn / dslectionn par un clic de souris ou par appui sur la barre d'espace.

Fonctionnement de l'application

L'utilisateur tape du texte dans le champ 1. Il l'ajoute la liste 1 avec le bouton Ajouter (2). Le champ de saisie (1) est alors vid et l'utilisateur peut ajouter un nouvel lment. Il peut transfrer des lments d'une liste l'autre en slectionnant l'lment transfrer dans l'une des listes et en choississant le bouton de transfert adquat 5 ou 6. L'lment transfr est ajout la fin de la liste de destination et enlev de la liste source. Il peut double-cliquer sur un lment de la liste 1. Cet lment est alors transfr dans la bote de saisie pour modification et enlev de la liste 1.

Les boutons sont allums ou teints selon les rgles suivantes :


le bouton Ajouter n'est allum que s'il y a un texte non vide dans le champ de saisie le bouton [5] de transfert de la liste 1 vers la liste 2 n'est allum que s'il y a un lment slectionn dans la liste 1 le bouton [6] de transfert de la liste 2 vers la liste 1 n'est allum que s'il y a un lment slectionn dans la liste 2 les boutons [7] et [8] d'effacement des listes 1 et 2 ne sont allums que si la liste effacer contient des lments.

Dans les conditions prcdentes, tous les boutons doivent tre teints lors du dmarrage de l'application. C'est la proprit Enabled des boutons qu'il faut alors positionner false. On peut le faire au moment de la conception ce qui aura pour effet de gnrer le code correspondant dans la mthode InitializeComponent ou le faire nous-mmes dans le constructeur comme ci-dessous :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. } public Form1() { InitializeComponent(); // --- initialisations complmentaires --// on inhibe un certain nombre de boutons buttonAjouter.Enabled = false; button1vers2.Enabled = false; button2vers1.Enabled = false; buttonEffacer1.Enabled = false; buttonEffacer2.Enabled = false;

L'tat du bouton Ajouter est contrl par le contenu du champ de saisie. C'est l'vnement TextChanged qui nous permet de suivre les changements de ce contenu :
1. 2. 3. 4. 5. 6. private void textBoxSaisie_TextChanged(object sender, System.EventArgs e) { // le contenu de textBoxSaisie a chang // le bouton Ajouter n'est allum que si la saisie est non vide buttonAjouter.Enabled = textBoxSaisie.Text.Trim() != ""; }

L'tat des boutons de transfert dpend du fait qu'un lment a t slectionn ou non dans la liste qu'ils contrlent :
1. 2. 3. private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) { // un lment a t slectionn // on allume le bouton de transfert 1 vers 2

Interfaces graphiques

185

4. 5. 6. 7. 8. 9. 10. 11. }

button1vers2.Enabled = true; } private void listBox2_SelectedIndexChanged(object sender, System.EventArgs e) { // un lment a t slectionn // on allume le bouton de transfert 2 vers 1 button2vers1.Enabled = true;

Le code associ au clic sur le bouton Ajouter est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. } private void buttonAjouter_Click(object sender, System.EventArgs e) { // ajout d'un nouvel lment la liste 1 listBox1.Items.Add(textBoxSaisie.Text.Trim()); // raz de la saisie textBoxSaisie.Text = ""; // Liste 1 n'est pas vide buttonEffacer1.Enabled = true; // retour du focus sur la bote de saisie textBoxSaisie.Focus();

On notera la mthode Focus qui permet de mettre le "focus" sur un contrle du formulaire. Le code associ au clic sur les boutons Effacer :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. } private void buttonEffacer1_Click(object sender, System.EventArgs e) { // on efface la liste 1 listBox1.Items.Clear(); // bouton Effacer buttonEffacer1.Enabled = false; } private void buttonEffacer2_Click(object sender, System.EventArgs e) { // on efface la liste 2 listBox2.Items.Clear(); // bouton Effacer buttonEffacer2.Enabled = false;

Le code de transfert des lments slectionns d'une liste vers l'autre :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. private void button1vers2_Click(object sender, System.EventArgs e) { // transfert de l'lment slectionn dans Liste 1 dans Liste 2 transfert(listBox1, button1vers2, buttonEffacer1, listBox2, button2vers1, buttonEffacer2); } private void button2vers1_Click(object sender, System.EventArgs e) { // transfert de l'lment slectionn dans Liste 2 dans Liste 1 transfert(listBox2, button2vers1, buttonEffacer2, listBox1, button1vers2, buttonEffacer1); }

Les deux mthodes ci-dessus dlguent le transfert des lments slectionns d'une liste l'autre une mme mthode prive appele transfert :
(a) (b) (c) (d) (e) (f) (g) (h) (i) (j) (k) (l) (m) (n) (o) (p) (q) // transfert private void transfert(ListBox l1, Button button1vers2, Button buttonEffacer1, ListBox l2, Button button2vers1, Button buttonEffacer2) { // transfert dans la liste l2 des lments slectionns de la liste l1 for (int i = l1.SelectedIndices.Count - 1; i >= 0; i--) { // index de l'lment slectionn int index = l1.SelectedIndices[i]; // ajout dans l2 l2.Items.Add(l1.Items[index]); // suppression dans l1 l1.Items.RemoveAt(index); } // boutons Effacer buttonEffacer2.Enabled = l2.Items.Count != 0; buttonEffacer1.Enabled = l1.Items.Count != 0; // boutons de transfert button1vers2.Enabled = false; }

ligne b : la mthode transfert reoit six paramtres :

Interfaces graphiques

186

une rfrence sur la liste contenant les lments slectionns appele ici l1. Lors de l'excution de l'application, l1 est soit listBox1 soit listBox2. On voit des exemples d'appel, lignes 3 et 8 des procdures de transfert buttonXversY_Click. une rfrence sur le bouton de transfert li la liste l1. Par exemple si l1 est listBox2, ce sera button2vers1( cf appel ligne 8) une rfrence sur le bouton d'effacement de la liste l1. Par exemple si l1 est listBox1, ce sera buttonEffacer1( cf appel ligne 3) les trois autres rfrences sont analogues mais font rfrence la liste l2. ligne d : la collection [ListBox].SelectedIndices reprsente les indices des lments slectionns dans le composant [ListBox]. C'est une collection : [ListBox].SelectedIndices.Count est le nombre d'lment de cette collection [ListBox].SelectedIndices[i] est l'lment n i de cette collection On parcourt la collection en sens inverse : on commence par la fin de la collection pour terminer par le dbut. Nous expliquerons pourquoi. ligne f : indice d'un lment slectionn de la liste l1 ligne h : cet lment est ajout dans la liste l2 ligne j : et supprim de la liste l1. Parce qu'il est supprim, il n'est plus slectionn. La collection l1.SelectedIndices de la ligne d va tre recalcule. Elle va perdre l'lment qui vient d'tre supprim. Tous les lments qui sont aprs celui-ci vont voir leur n passer de n n-1. si la boucle de la ligne (d) est croissante et qu'elle vient de traiter l'lment n 0, elle va ensuite traiter l'lment n 1. Or l'lment qui portait le n 1 avant la suppression de l'lment n 0, va ensuite porter le n 0. Il sera alors oubli par la boucle. si la boucle de la ligne (d) est dcroissante et qu'elle vient de traiter l'lment n n, elle va ensuite traiter l'lment n n-1. Aprs suppression de l'lment n n, l'lment n n-1 ne change pas de n. Il est donc trait au tour de boucle suivant. lignes m-n : l'tat des boutons [Effacer] dpend de la prsence ou non d'lments dans les listes associes ligne p : la liste l2 n'a plus d'lments slectionns : on teint son bouton de transfert.

5.2.5

Cases cocher CheckBox, boutons radio ButtonRadio

Nous nous proposons d'crire l'application suivante :

2 3 4 5 6 7

Les composants de la fentre sont les suivants : n type nom 1 GroupBox groupBox1 cf [6] 2 RadioButton radioButton1 radioButton2 radioButton3 3 4 5 GroupBox CheckBox ListBox rle un conteneur de composants. On peut y dposer d'autres composants. Text=Boutons radio 3 boutons radio - radioButton1 a la proprit Checked=True et la proprit Text=1 - radioButton2 a la proprit Text=2 - radioButton3 a la proprit Text=3 Des boutons radio prsents dans un mme conteneur, ici le GroupBox, sont exclusifs l'un de l'autre : seul l'un d'entre-eux est allum.

groupBox2 checkBox1 3 cases cocher. chechBox1 a la proprit Checked=True et la proprit Text=A - chechBox2 a la checkBox2 proprit Text=B - chechBox3 a la proprit Text=C checkBox3 listBoxValeurs une liste qui affiche les valeurs des boutons radio et des cases cocher ds qu'un changement

Interfaces graphiques

187

intervient. montre o trouver le conteneur GroupBox

L'vnement qui nous intresse pour ces six contrles est l'vnement CheckChanged indiquant que l'tat de la case cocher ou du bouton radio a chang. Cet tat est reprsent dans les deux cas par la proprit boolenne Checked qui vrai signifie que le contrle est coch. Nous n'utiliserons ici qu'une seule mthode pour traiter les six vnements CheckChanged, la mthode affiche. Pour faire en sorte que les six vnements CheckChanged soient grs par la mme mthode affiche, on pourra procder comme suit : Slectionnons le composant radioButton1 et cliquons droit dessus pour avoir accs ses proprits : 3 1 2 4

Dans l'onglet vnements [1], on associe la mthode affiche [2] l'vnement CheckChanged. Cela signifie que l'on souhaite que le clic sur l'option A1 soit traite par une mthode appele affiche. Visual studio gnre automatiquement la mthode affiche dans la fentre de code :
1. 2. private void affiche(object sender, EventArgs e) { }

La mthode affiche est une mthode de type EventHandler. Pour les cinq autres composants, on procde de mme. Slectionnons par exemple l'option CheckBox1 et ses vnements [3]. En face de l'vnement Click, on a une liste droulante [4] dans laquelle sont prsentes les mthodes existantes pouvant traiter cet vnement. Ici on n'a que la mthode affiche. On la slectionne. On rpte ce processus pour tous les autres composants. Dans la mthode InitializeComponent du code a t gnr. La mthode affiche a t dclare comme gestionnaire des six vnements CheckedChanged de la faon suivante :
1. 2. 3. 4. 5. 6. this.radioButton1.CheckedChanged this.radioButton2.CheckedChanged this.radioButton3.CheckedChanged this.checkBox1.CheckedChanged += this.checkBox2.CheckedChanged += this.checkBox3.CheckedChanged += += new System.EventHandler(this.affiche); += new System.EventHandler(this.affiche); += new System.EventHandler(this.affiche); new System.EventHandler(this.affiche); new System.EventHandler(this.affiche); new System.EventHandler(this.affiche);

La mthode affiche est complte comme suit :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. } private void affiche(object sender, System.EventArgs e) { // affiche l'tat du bouton radio ou de la case cocher // est-ce un checkbox ? if (sender is CheckBox) { CheckBox chk = (CheckBox)sender; listBoxvaleurs.Items.Add(chk.Name + "=" + chk.Checked); } // est-ce un radiobutton ? if (sender is RadioButton) { RadioButton rdb = (RadioButton)sender; listBoxvaleurs.Items.Add(rdb.Name + "=" + rdb.Checked); }

La syntaxe
if (sender is CheckBox) {

permet de vrifier que l'objet sender est de type CheckBox. Cela nous permet ensuite de faire un transtypage vers le type exact de sender. La mthode affiche crit dans la liste listBoxValeurs le nom du composant l'origine de l'vnement et la valeur de sa proprit Checked. A l'excution [7], on voit qu'un clic sur un bouton radio provoque deux vnements CheckChanged : l'un sur l'ancien bouton coch qui passe "non coch" et l'autre sur le nouveau bouton qui passe "coch".

Interfaces graphiques

188

5.2.6

Variateurs ScrollBar

Il existe plusieurs types de variateur : le variateur horizontal (HScrollBar), le variateur vertical (VScrollBar), l'incrmenteur (NumericUpDown).

Ralisons l'application suivante :

1 2

3 4

n 1 2 3 4

type hScrollBar hScrollBar Label NumericUpDown


nom hScrollBar1 hScrollBar2 labelValeurHS1 numericUpDown2

rle un variateur horizontal un variateur horizontal qui suit les variations du variateur 1 affiche la valeur du variateur horizontal permet de fixer la valeur du variateur 2

Un variateur ScrollBar permet l'utilisateur de choisir une valeur dans une plage de valeurs entires symbolise par la "bande" du variateur sur laquelle se dplace un curseur. La valeur du variateur est disponible dans sa proprit Value. Pour un variateur horizontal, l'extrmit gauche reprsente la valeur minimale de la plage, l'extrmit droite la valeur maximale, le curseur la valeur actuelle choisie. Pour un variateur vertical, le minimum est reprsent par l'extrmit haute, le maximum par l'extrmit basse. Ces valeurs sont reprsentes par les proprits Minimum et Maximum et valent par dfaut 0 et 100. Un clic sur les extrmits du variateur fait varier la valeur d'un incrment (positif ou ngatif) selon l'extrmit clique appele SmallChange qui vaut par dfaut 1. Un clic de part et d'autre du curseur fait varier la valeur d'un incrment (positif ou ngatif) selon l'extrmit clique appele LargeChange qui vaut par dfaut 10. Lorsqu'on clique sur l'extrmit suprieure d'un variateur vertical, sa valeur diminue. Cela peut surprendre l'utilisateur moyen qui s'attend normalement voir la valeur "monter". On rgle ce problme en donnant une valeur ngative aux proprits SmallChange et LargeChange Ces cinq proprits (Value, Minimum, Maximum, SmallChange, LargeChange) sont accessibles en lecture et criture. L'vnement principal du variateur est celui qui signale un changement de valeur : l'vnement Scroll.

Un composant NumericUpDown est proche du variateur : il a lui aussi les proprits Minimum, Maximum et Value, par dfaut 0, 100, 0. Mais ici, la proprit Value est affiche dans une bote de saisie faisant partie intgrante du contrle. L'utilisateur peut lui mme modifier cette valeur sauf si on a mis la proprit ReadOnly du contrle vrai. La valeur de l'incrment est fixe par la proprit Increment, par dfaut 1. L'vnement principal du composant NumericUpDown est celui qui signale un changement de valeur : l'vnement ValueChanged Le code de l'application est le suivant :
1. 2. 3. 4. 5. 6. 7. using System.Windows.Forms; namespace Chap5 { public partial class Form1 : Form { public Form1() { InitializeComponent(); // on fixe les caractristiques du variateur 1

Interfaces graphiques

189

8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. } 42. }

hScrollBar1.Value = 7; hScrollBar1.Minimum = 1; hScrollBar1.Maximum = 130; hScrollBar1.LargeChange = 11; hScrollBar1.SmallChange = 1; // on donne au variateur 2 les mmes caractristiques qu'au variateur 1 hScrollBar2.Value = hScrollBar1.Value; hScrollBar2.Minimum = hScrollBar1.Minimum; hScrollBar2.Maximum = hScrollBar1.Maximum; hScrollBar2.LargeChange = hScrollBar1.LargeChange; hScrollBar2.SmallChange = hScrollBar1.SmallChange; // idem pour l'incrmenteur numericUpDown2.Value = hScrollBar1.Value; numericUpDown2.Minimum = hScrollBar1.Minimum; numericUpDown2.Maximum = hScrollBar1.Maximum; numericUpDown2.Increment = hScrollBar1.SmallChange; // on donne au Label la valeur du variateur 1 labelValeurHS1.Text = hScrollBar1.Value.ToString(); } private void hScrollBar1_Scroll(object sender, ScrollEventArgs e) { // changement de valeur du variateur 1 // on rpercute sa valeur sur le variateur 2 et sur le label hScrollBar2.Value = hScrollBar1.Value; labelValeurHS1.Text = hScrollBar1.Value.ToString(); } private void numericUpDown2_ValueChanged(object sender, System.EventArgs e) { // l'incrmenteur a chang de valeur // on fixe la valeur du variateur 2 hScrollBar2.Value = (int)numericUpDown2.Value; }

5.3

vnements souris

Lorsqu'on dessine dans un conteneur, il est important de connatre la position de la souris pour, par exemple, afficher un point lors d'un clic. Les dplacements de la souris provoquent des vnements dans le conteneur dans lequel elle se dplace.

[1] : les vnements survenant lors d'un dplacement de la souris sur le formulaire ou sur un contrle [2] : les vnements survenant lors d'un glisser / lcher (Drag'nDrop) la souris vient d'entrer dans le domaine du contrle la souris vient de quitter le domaine du contrle la souris bouge dans le domaine du contrle Pression sur le bouton gauche de la souris Relchement du bouton gauche de la souris l'utilisateur lche un objet sur le contrle l'utilisateur entre dans le domaine du contrle en tirant un objet l'utilisateur sort du domaine du contrle en tirant un objet l'utilisateur passe au-dessus domaine du contrle en tirant un objet

MouseEnter MouseLeave MouseMove MouseDown MouseUp DragDrop DragEnter DragLeave DragOver

Voici une application permettant de mieux apprhender quels moments se produisent les diffrents vnements souris :

Interfaces graphiques

190

1 2 3

n type nom rle 1 Label lblPositionSouris pour afficher la position de la souris dans le formulaire 1, la liste 2 ou le bouton 3 2 ListBox listBoxEvts pour afficher les vts souris autres que MouseMove 3 Button buttonEffacer pour effacer le contenu de 2 Pour suivre les dplacements de la souris sur les trois contrles, on n'crit qu'un seul gestionnaire, le gestionnaire affiche :

Le code de la procdure affiche est le suivant :


1. 2. 3. 4. private void affiche(object sender, MouseEventArgs e) { // mvt souris - on affiche les coordonnes (X,Y) de celle-ci labelPositionSouris.Text = "(" + e.X + "," + e.Y + ")";

A chaque fois que la souris entre dans le domaine d'un contrle son systme de coordonnes change. Son origine (0,0) est le coin suprieur gauche du contrle sur lequel elle se trouve. Ainsi l'excution, lorsqu'on passe la souris du formulaire au bouton, on voit clairement le changement de coordonnes. Afin de mieux voir ces changements de domaine de la souris, on peut utiliser la proprit Cursor [1] des contrles : 4 1 2 3

Cette proprit permet de fixer la forme du curseur de souris lorsque celle-ci entre dans le domaine du contrle. Ainsi dans notre exemple, nous avons fix le curseur Default pour le formulaire lui-mme [2], Hand pour la liste 2 [3] et Cross pour le bouton 3 [4]. Par ailleurs, pour dtecter les entres et sorties de la souris sur la liste 2, nous traitons les vnements MouseEnter et MouseLeave de cette mme liste :
1. 2. private void listBoxEvts_MouseEnter(object sender, System.EventArgs e) { // on signale l'vt

Interfaces graphiques

191

3. 4. 5. 6. 7. 8. 9.

listBoxEvts.Items.Insert(0, string.Format("MouseEnter {0:hh:mm:ss}",DateTime.Now)); } private void listBoxEvts_MouseLeave(object sender, EventArgs e) { // on signale l'vt listBoxEvts.Items.Insert(0, string.Format("MouseLeave {0:hh:mm:ss}", DateTime.Now));

Pour traiter les clics sur le formulaire, nous traitons les vnements MouseDown et MouseUp :
1. 2. 3. 4. 5. 6. 7. 8. 9. private void listBoxEvts_MouseDown(object sender, MouseEventArgs e) { // on signale l'vt listBoxEvts.Items.Insert(0, string.Format("MouseDown {0:hh:mm:ss}", DateTime.Now)); } private void listBoxEvts_MouseUp(object sender, MouseEventArgs e) { // on signale l'vt listBoxEvts.Items.Insert(0, string.Format("MouseUp {0:hh:mm:ss}", DateTime.Now));

lignes 3 et 8 : les messages sont placs en 1re position dans le ListBox afin que les vnements les plus rcents soient les premiers dans la liste.

Enfin, le code du gestionnaire de clic sur le bouton Effacer :


1. 2. 3. private void buttonEffacer_Click(object sender, EventArgs e) { listBoxEvts.Items.Clear(); }

5.4

Crer une fentre avec menu

Voyons maintenant comment crer une fentre avec menu. Nous allons crer la fentre suivante :

2 1

Pour crer un menu, on choisit le composant "MenuStrip" dans la barre "Menus & Tollbars" :

Interfaces graphiques

192

3 4 5

[1] : choix du composant [MenuStrip] [2] : on a alors un menu qui s'installe sur le formulaire avec des cases vides intitules "Type Here". Il suffit d'y indiquer les diffrentes options du menu. [3] : le libell "Options A" a t tap. On passe au libell [4]. [5] : les libells des options A ont t saisis. On passe au libell [6]

6 8 9

[6] : les premires options B [7] : sous B1, on met un sparateur. Celui-ci est disponible dans un combo associ au texte "Type Here" [8] : pour faire un sous-menu, utiliser la flche [8] et taper le sous-menu dans [9]

Il reste nommer le diffrents composants du formulaire :

2 1

n type nom(s) 1 Label labelStatut 2 toolStripMenuItem toolStripMenuItemOptionsA toolStripMenuItemA1 toolStripMenuItemA2 toolStripMenuItemA3 3 toolStripMenuItem toolStripMenuItemOptionsB toolStripMenuItemB1 toolStripMenuItemB2 toolStripMenuItemB3 4 toolStripMenuItem toolStripMenuItemB31 toolStripMenuItemB32

rle pour afficher le texte de l'option de menu clique options de menu sous l'option principale "Options A"

options de menu sous l'option principale "Options B"

options de menu sous l'option principale "B3"

Interfaces graphiques

193

Les options de menu sont des contrles comme les autres composants visuels et ont des proprits et vnements. Par exemple les proprits de l'option de menu A1 sont les suivantes :

Deux proprits sont utilises dans notre exemple :


Name Text

le nom du contrle menu le libell de l'option de menu

Dans la structure du menu, slectionnons l'option A1 et cliquons droit pour avoir accs aux proprits du contrle :

Dans l'onglet vnements [1], on associe la mthode affiche [2] l'vnement Click. Cela signifie que l'on souhaite que le clic sur l'option A1 soit traite par une mthode appele affiche. Visual studio gnre automatiquement la mthode affiche dans la fentre de code :
3. 4. private void affiche(object sender, EventArgs e) { }

Dans cette mthode, nous nous contenterons d'afficher dans le label labelStatut la proprit Text de l'option de menu qui a t clique :
1. 2. 3. 4. private void affiche(object sender, EventArgs e) { // affiche dans le TextBox le nom du sous-menu choisi labelStatut.Text = ((ToolStripMenuItem)sender).Text; }

La source de l'vnement sender est de type object. Les options de menu sont elle de type ToolStripMenuItem, aussi est-on oblig de faire un transtypage de object vers ToolStripMenuItem. Pour toutes les options de menu, on fixe le gestionnaire du clic la mthode affiche [3,4]. Excutons l'application et slectionnons un lment de menu :

Interfaces graphiques

194

5.5

Composants non visuels

Nous nous intressons maintenant un certain nombre de composants non visuels : on les utilise lors de la conception mais on ne les voit pas lors de l'excution.

5.5.1

Botes de dialogue OpenFileDialog et SaveFileDialog

Nous allons construire l'application suivante :

5 1

Les contrles sont les suivants : N type 1 TextBox 2 3 4 5 rle texte tap par l'utilisateur ou charg partir d'un fichier MultiLine=True, ScrollBars=Both, AccepReturn=True, AcceptTab=True Button buttonSauvegarder permet de sauvegarder le texte de [1] dans un fichier texte Button buttonCharger permet de charger le contenu d'un fichier texte dans [1] Button buttonEffacer efface le contenu de [1] SaveFileDialog saveFileDialog1 composant permettant de choisir le nom et l'emplacement du fichier de sauvegarde de [1]. Ce composant est pris dans la barre d'outils [7] et simplement dpos sur le formulaire. Il est alors enregistr mais il n'occupe pas de place sur le formulaire. C'est un composant non visuel. OpenFileDialog openFileDialog1 composant permettant de choisir le fichier charger dans [1]. nom TextBoxLignes

Le code associ au bouton Effacer est simple :


1. 2. 3. 4. private void buttonEffacer_Click(object sender, EventArgs e) { // on met la chane vide dans le TexBox textBoxLignes.Text = "";

Nous utiliserons les proprits et mthodes suivantes de la classe SaveFileDialog : Champ Type Rle

Interfaces graphiques

195

string Filter

Proprit les types de fichiers proposs dans la liste droulante des types de fichiers de la bote de dialogue Proprit le n du type de fichier propos par dfaut dans la liste ci-dessus. Commence 0. Proprit le dossier prsent initialement pour la sauvegarde du fichier Proprit le nom du fichier de sauvegarde indiqu par l'utilisateur Mthode mthode qui affiche la bote de dialogue de sauvegarde. Rend un rsultat de type DialogResult.

int FilterIndex string InitialDirectory string FileName DialogResult.ShowDialog()

La mthode ShowDialog affiche une bote de dialogue analogue la suivante :

4 3 1

1 2 3 4

liste droulante construite partir de la proprit Filter. Le type de fichier propos par dfaut est fix par FilterIndex dossier courant, fix par InitialDirectory si cette proprit a t renseigne nom du fichier choisi ou tap directement par l'utilisateur. Sera disponible dans la proprit FileName boutons Enregistrer/Annuler. Si le bouton Enregistrer est utilis, la fonction ShowDialog rend le rsultat DialogResult.OK

La procdure de sauvegarde peut s'crire ainsi :


1. private void buttonSauvegarder_Click(object sender, System.EventArgs e) { 2. // on sauvegarde la bote de saisie dans un fichier texte 3. // on paramtre la bote de dialogue savefileDialog1 4. saveFileDialog1.InitialDirectory = Application.ExecutablePath; 5. saveFileDialog1.Filter = "Fichiers texte (*.txt)|*.txt|Tous les fichiers (*.*)|*.*"; 6. saveFileDialog1.FilterIndex = 0; 7. // on affiche la bote de dialogue et on rcupre son rsultat 8. if (saveFileDialog1.ShowDialog() == DialogResult.OK) { 9. // on rcupre le nom du fichier 10. string nomFichier = saveFileDialog1.FileName; 11. StreamWriter fichier = null; 12. try { 13. // on ouvre le fichier en criture 14. fichier = new StreamWriter(nomFichier); 15. // on crit le texte dedans 16. fichier.Write(textBoxLignes.Text); 17. } catch (Exception ex) { 18. // problme 19. MessageBox.Show("Problme l'criture du fichier (" + 20. ex.Message + ")", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); 21. return;

Interfaces graphiques

196

22. 23. 24. 25. 26. 27. 28. 29.

} }

} finally { // on ferme le fichier if (fichier != null) { fichier.Dispose(); } }

ligne 4 : on fixe le dossier initial (InitialDirectory) au dossier (Application.ExecutablePath) qui contient l'excutable de l'application. ligne 5 : on fixe les types de fichiers prsenter. On notera la syntaxe des filtres : filtre1|filtre2|..|filtren avec filtrei= Texte| modle de fichier. Ici l'utilisateur aura le choix entre les fichiers *.txt et *.*. ligne 6 : on fixe le type de fichier prsenter en premier l'utilisateur. Ici l'index 0 dsigne les fichiers *.txt. ligne 8 : la bote de dialogue est affiche et son rsultat rcupr. Pendant que la bote de dialogue est affiche, l'utilisateur n'a plus accs au formulaire principal (bote de dialogue dite modale). L'utilisateur fixe le nom du fichier sauvegarder et quitte la bote soit par le bouton Enregistrer, soit par le bouton Annuler, soit en fermant la bote. Le rsultat de la mthode ShowDialog est DialogResult.OK uniquement si l'utilisateur a utilis le bouton Enregistrer pour quitter la bote de dialogue. Ceci fait, le nom du fichier crer est maintenant dans la proprit FileName de l'objet saveFileDialog1. On est alors ramen la cration classique d'un fichier texte. On y crit le contenu du TextBox : textBoxLignes.Text tout en grant les exceptions qui peuvent se produire.

La classe OpenFileDialog est trs proche de la classe SaveFileDialog. On utilisera les mmes mthodes et proprits que prcdemment. La mthode ShowDialog affiche une bote de dialogue analogue la suivante :

4 3 1

1 2 3 4

liste droulante construite partir de la proprit Filter. Le type de fichier propos par dfaut est fix par FilterIndex dossier courant, fix par InitialDirectory si cette proprit a t renseigne nom du fichier choisi ou tap directement par l'utilisateur. Sera disponible dans la proprit FileName boutons Ouvrir/Annuler. Si le bouton Ouvrir est utilis, la fonction ShowDialog rend le rsultat DialogResult.OK

La procdure de chargement du fichier texte peut s'crire ainsi :


1. 2. 3. 4. 5. 6. 7. 8. 9. private void buttonCharger_Click(object sender, EventArgs e) { // on charge un fichier texte dans la bote de saisie // on paramtre la bote de dialogue openfileDialog1 openFileDialog1.InitialDirectory = Application.ExecutablePath; openFileDialog1.Filter = "Fichiers texte (*.txt)|*.txt|Tous les fichiers (*.*)|*.*"; openFileDialog1.FilterIndex = 0; // on affiche la bote de dialogue et on rcupre son rsultat if (openFileDialog1.ShowDialog() == DialogResult.OK) { // on rcupre le nom du fichier

Interfaces graphiques

197

10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29.

string nomFichier = openFileDialog1.FileName; StreamReader fichier = null; try { // on ouvre le fichier en lecture fichier = new StreamReader(nomFichier); // on lit tout le fichier et on le met dans le TextBox textBoxLignes.Text = fichier.ReadToEnd(); } catch (Exception ex) { // problme MessageBox.Show("Problme la lecture du fichier (" + ex.Message + ")", "Erreur", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } finally { // on ferme le fichier if (fichier != null) { fichier.Dispose(); } }//finally }//if }

ligne 4 : on fixe le dossier initial (InitialDirectory) au dossier (Application.ExecutablePath) qui contient l'excutable de l'application. ligne 5 : on fixe les types de fichiers prsenter. On notera la syntaxe des filtres : filtre1|filtre2|..|filtren avec filtrei= Texte| modle de fichier. Ici l'utilisateur aura le choix entre les fichiers *.txt et *.*. ligne 6 : on fixe le type de fichier prsenter en premier l'utilisateur. Ici l'index 0 dsigne les fichiers *.txt. ligne 8 : la bote de dialogue est affiche et son rsultat rcupr. Pendant que la bote de dialogue est affiche, l'utilisateur n'a plus accs au formulaire principal (bote de dialogue dite modale). L'utilisateur fixe le nom du fichier sauvegarder et quitte la bote soit par le bouton Ouvrir, soit par le bouton Annuler, soit en fermant la bote. Le rsultat de la mthode ShowDialog est DialogResult.OK uniquement si l'utilisateur a utilis le bouton Enregistrer pour quitter la bote de dialogue. Ceci fait, le nom du fichier crer est maintenant dans la proprit FileName de l'objet openFileDialog1. On est alors ramen la lecture classique d'un fichier texte. On notera, ligne 16, la mthode qui permet de lire la totalit d'un fichier.

5.5.2

Botes de dialogue FontColor et ColorDialog

Nous continuons l'exemple prcdent en y ajoutant deux nouveaux boutons et deux nouveaux contrles non visuels :

4 5

1 6

N 1 2 3 4

type Button Button ColorDialog FontDialog

nom buttonCouleur buttonPolice colorDialog1 colorDialog1

rle pour fixer la couleur des caractres du TextBox pour fixer la police de caractres du TextBox le composant qui permet la slection d'une couleur - pris dans la bote outils [5]. le composant qui permet la slection d'une police de caractres - pris dans la bote outils [5].

Les classes FontDialog et ColorDialog ont une mthode ShowDialog analogue la mthode ShowDialog des classes OpenFileDialog et SaveFileDialog. La mthode ShowDialog de la classe ColorDialog permet de choisir une couleur [1]. Celle de la classe FontDialog permet de choisir une police de caractres [2] :

Interfaces graphiques

198

[1] : si l'utilisateur quitte la bote de dialogue avec le bouton OK, le rsultat de la mthode ShowDialog est DialogResult.OK et la couleur choisie est dans la proprit Color de l'objet ColorDialog utilis. [2] : si l'utilisateur quitte la bote de dialogue avec le bouton OK, le rsultat de la mthode ShowDialog est DialogResult.OK et la police choisie est dans la proprit Font de l'objet FontDialog utilis.

Nous avons dsormais les lments pour traiter les clics sur les boutons Couleur et Police :
1. texte private void buttonCouleur_Click(object sender, EventArgs e) {// choix d'une couleur de if (colorDialog1.ShowDialog() == DialogResult.OK) { // on change la proprit Forecolor du TextBox textBoxLignes.ForeColor = colorDialog1.Color; }//if } private void buttonPolice_Click(object sender, EventArgs e) { // choix d'une police de caractres if (fontDialog1.ShowDialog() == DialogResult.OK) { // on change la proprit Font du TextBox textBoxLignes.Font = fontDialog1.Font;

2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. }

ligne [4] : la proprit [ForeColor] d'un composant TextBox dsigne la couleur de type [Color] des caractres du TextBox. Ici cette couleur est celle choisie par l'utilisateur dans la bote de dialogue de type [ColorDialog]. ligne [12] : la proprit [Font] d'un composant TextBox dsigne la police de caractres de type [Font] des caractres du TextBox. Ici cette police est celle choisie par l'utilisateur dans la bote de dialogue de type [FontDialog].

5.5.3

Timer

Nous nous proposons ici d'crire l'application suivante :

4 1 2

Interfaces graphiques

199

n Type Nom Rle 1 Label labelChrono affiche un chronomtre 2 Button buttonArretMarche bouton Arrt/Marche du chronomtre 3 Timer timer1 composant mettant ici un vnement toutes les secondes En [4], nous voyons le chronomtre en marche, en [5] le chronomtre arrt. Pour changer toutes les secondes le contenu du Label LabelChrono, il nous faut un composant qui gnre un vnement toutes les secondes, vnement qu'on pourra intercepter pour mettre jour l'affichage du chronomtre. Ce composant c'est le Timer [1] disponible dans la bote outils Components [2] : 2

Les proprits du composant Timer utilises ici seront les suivantes :


Interval Tick Enabled

nombre de millisecondes au bout duquel un vnement Tick est mis. l'vnement produit la fin de Interval millisecondes rend le timer actif (true) ou inactif (false)

Dans notre exemple le timer s'appelle timer1 et timer1.Interval est mis 1000 ms (1s). L'vnement Tick se produira donc toutes les secondes. Le clic sur le bouton Arrt/Marche est trait par la procdure buttonArretMarche_Click suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; using System.Windows.Forms; namespace Chap5 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } // variable d'instance private DateTime dbut = DateTime.Now; ... private void buttonArretMarche_Click(object sender, EventArgs e) { // arrt ou marche ? if (buttonArretMarche.Text == "Marche") { // on note l'heure de dbut dbut = DateTime.Now; // on l'affiche labelChrono.Text = "00:00:00"; // on lance le timer timer1.Enabled = true; // on change le libell du bouton buttonArretMarche.Text = "Arrt"; // fin return; }// if (buttonArretMarche.Text == "Arrt") { // arrt du timer

Interfaces graphiques

200

29. 30. 31. 32. 33. 34. 35. 36. 37. } 38. }

timer1.Enabled = false; // on change le libell du bouton buttonArretMarche.Text = "Marche"; // fin return; } }

ligne 13 : la procdure qui traite le clic sur le bouton Arrt/Marche. ligne 15 : le libell du bouton Arrt/Marche est soit "Arrt" soit "Marche". On est donc oblig de faire un test sur ce libell pour savoir quoi faire. ligne 17 : dans le cas de "Marche", on note l'heure de dbut dans une variable dbut qui est une variable globale (ligne 11) de l'objet formulaire ligne 19 : initialise le contenu du label LabelChrono ligne 21 : le timer est lanc (Enabled=true) ligne 23 : libell du bouton passe "Arrt". ligne 27 : dans le cas de "Arrt" ligne 29 : on arrte le timer (Enabled=false) ligne 31 : on passe le libell du bouton "Marche".

Il nous reste traiter l'vnement Tick sur l'objet timer1, vnement qui se produit toutes les secondes :
1. 2. 3. 4. 5. 6. 7. private void timer1_Tick(object sender, EventArgs e) { // une seconde s'est coule DateTime maintenant = DateTime.Now; TimeSpan dure = maintenant - dbut; // on met jour le chronomtre labelChrono.Text = dure.Hours.ToString("d2") + ":" + dure.Minutes.ToString("d2") + ":" + dure.Seconds.ToString("d2"); }

ligne 3 : on note l'heure du moment ligne 4 : on calcule le temps coul depuis l'heure de lancement du chronomtre. On obtient un objet de type TimeSpan qui reprsente une dure dans le temps. ligne 6 : celle-ci doit tre affiche dans le chronomtre sous la forme hh:mm:ss. Pour cela nous utilisons les proprits Hours, Minutes, Seconds de l'objet TimeSPan qui reprsentent respectivement les heures, minutes, secondes de la dure que nous affichons au format ToString("d2") pour avoir un affichage sur 2 chiffres.

5.6

Application exemple - version 6

On reprend l'application exemple IMPOTS. La dernire version a t tudie au paragraphe 4.4, page 138. C'tait l'application trois couches suivante :

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL 3 2 SPRING

Couche d'accs aux donnes [dao] DLL 1

Donnes

les couches [metier] et [dao] taient encapsules dans des DLL la couche [ui] tait une couche [console] l'instanciation des couches et leur intgration dans l'application taient assures par Spring.

Dans cette nouvelle version, la couche [ui] sera assure par l'interface graphique suivante :

Interfaces graphiques

201

5.6.1

La solution Visual Studio

La solution Visual Studio est compose des lments suivants : 1 2 3

[1] : le projet est constitu des lments suivants : [Program.cs] : la classe qui lance l'application [Form1.cs] : la classe d'un 1er formulaire [Form2] : la classe d'un 2ime formulaire [lib] dtaill dans [2] : on y a mis toutes les DLL ncessaires au projet : [ImpotsV5-dao.dll] : la DLL de la couche [dao] gnre au paragraphe 4.4.3, page 151 [ImpotsV5-metier.dll] : la DLL de la couche [dao] gnre au paragraphe 4.4.4, page 155 [Spring.Core.dll], [Common.Logging.dll], [antlr.runtime.dll] : les DLL de Spring dj utilises dans la version prcdente (cf paragraphe 4.4.6, page 156). [references] dtaill dans [3] : les rfrences du projet. On a ajout une rfrence pour chacune des DLL du dossier [lib] [App.config] : le fichier de configuration du projet. Il est identique celui de la version prcdente dcrit au paragraphe 4.4.6, page 161. [DataImpot.txt] : le fichier des tranches d'impt configur pour tre recopi automatiquement dans le dossier d'excution du projet [4]

Le formulaire [Form1] est le formulaire de saisies des paramtres du calcul de l'impt [A] dj prsent plus haut. Le formulaire [Form2] [B] sert afficher un message d'erreur :

Interfaces graphiques

202

5.6.2

La classe [Program.cs]

La classe [Program.cs] lance l'application. Son code est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. using using using using using using System; System.Windows.Forms; Spring.Context; Spring.Context.Support; Metier; System.Text;

namespace Chap5 { static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() { // code gnr par Vs Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // --------------- Code dveloppeur // instanciations couches [metier] et [dao] IApplicationContext ctx = null; Exception ex = null; IImpotMetier metier = null; try { // contexte Spring ctx = ContextRegistry.GetContext(); // on demande une rfrence sur la couche [metier] metier = (IImpotMetier)ctx.GetObject("metier"); } catch (Exception e1) { // mmorisation exception ex = e1; } // formulaire afficher Form form = null; // y-a-t-il eu une exception ? if (ex != null) { // oui - on cre le message d'erreur afficher StringBuilder msgErreur = new StringBuilder(String.Format("Chane des exceptions : {0} {1}", "".PadLeft(40, '-'), Environment.NewLine)); Exception e = ex; while (e != null) { msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine)); msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine)); e = e.InnerException; } // cration fentre d'erreur laquelle on passe le message d'erreur afficher Form2 form2 = new Form2(); form2.MsgErreur = msgErreur.ToString();

Interfaces graphiques

203

48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62.

// ce sera la fentre afficher form = form2; } else { // tout s'est bien pass // cration interface graphique [Form1] laquelle on passe la rfrence sur la couche [metier] Form1 form1 = new Form1(); form1.Metier = metier; // ce sera la fentre afficher form = form1; } // affichage fentre Application.Run(form); } } }

Le code gnr par Visual Studio a t complt partir de la ligne 19. L'application exploite le fichier [App.config] suivant :
(a) (b) (c) (d) (e) (f) (g) (h) (i) (j) (k) (l) (m) (n) (o) (p) (q) (r) (s) (t) (u) (v) (w) (x) <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" /> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" /> </sectionGroup> </configSections> <spring> <context> <resource uri="config://spring/objects" /> </context> <objects xmlns="http://www.springframework.net"> <object name="dao" type="Dao.FileImpot, ImpotsV5-dao"> <constructor-arg index="0" value="DataImpot.txt"/> </object> <object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier"> <constructor-arg index="0" ref="dao"/> </object> </objects> </spring> </configuration>

lignes 24-32 : exploitation du fichier [App.config] prcdent pour instancier les couches [metier] et [dao] ligne 26 : exploitation du fichier [App.config] ligne 28 : rcupration d'une rfrence sur la couche [metier] ligne 31 : mmorisation de l'ventuelle exception ligne 34 : la rfrence form dsignera le formulaire afficher (form1 ou form2) lignes 36-50 : s'il y a eu exception, on se prpare afficher un formulaire de type [Form2] lignes 38-44 : on construit le message d'erreur afficher. Il est constitu de la concatnation des messages d'erreurs des diffrentes exceptions prsentes dans la chane des exceptions. ligne 46 : un formulaire de type [Form2] est cr. ligne 47 : nous le verrons ultrieurement, ce formulaire a une proprit publique MsgErreur qui est le message d'erreur afficher :
public string MsgErreur { private get; set; }

On renseigne cette proprit. ligne 49 : la rfrence form qui dsigne la fentre afficher est initialise. On notera le polymorphisme l'oeuvre. form2 n'est pas de type [Form] mais de type [Form2], un type driv de [Form]. lignes 50-57 : il n'y a pas eu d'exception. On se prpare afficher un formulaire de type [Form1]. ligne 53 : un formulaire de type [Form1] est cr. ligne 54 : nous le verrons ultrieurement, ce formulaire a une proprit publique Metier qui est une rfrence sur la couche [metier] :
public IImpotMetier Metier { private get; set; }

On renseigne cette proprit.

Interfaces graphiques

204

ligne 56 : la rfrence form qui dsigne la fentre afficher est initialise. On notera de nouveau le polymorphisme l'oeuvre. form1 n'est pas de type [Form] mais de type [Form1], un type driv de [Form]. ligne 59 : la fentre rfrence par form est affiche.

5.6.3

Le formulaire [Form1]

En mode [conception] le formulaire [Form1] est le suivant :

2 3 4 5 6 7 8 9

Les contrles sont les suivants n type 0 GroupBox 1 RadioButton 2 RadioButton 3 4 5 6 7 8 rle Text=Etes-vous mari(e) coch si mari coch si pas mari Checked=True NumericUpDown numericUpDownEnfants nombre d'enfants du contribuable Minimum=0, Maximum=20, Increment=1 TextBox textSalaire salaire annuel du contribuable en euros Label labelImpot montant de l'impt payer BorderStyle=Fixed3D Button buttonCalculer lance le calcul de l'impt Button buttonEffacer remet le formulaire dans l'tat qu'il avait lors du chargement Button buttonQuitter pour quitter l'application nom groupBox1 radioButtonOui radioButtonNon

Rgles de fonctionnement du formulaire le bouton Calculer reste teint tant qu'il n'y a rien dans le champ du salaire si lorsque le calcul est lanc, il s'avre que le salaire est incorrect, l'erreur est signale [9]

Le code de la classe est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. using System.Windows.Forms; using Metier; using System; namespace Chap5 { public partial class Form1 : Form { // couche [mtier] public IImpotMetier Metier { private get; set; } public Form1() { InitializeComponent(); } private void buttonCalculer_Click(object sender, System.EventArgs e) { // le salaire est-il correct

Interfaces graphiques

205

16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29.

30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. } 50. }

int salaire; bool ok=int.TryParse(textSalaire.Text.Trim(), out salaire); if (! ok || salaire < 0) { // msg d'erreur MessageBox.Show("Salaire incorrect", "Erreur de saisie", MessageBoxButtons.OK, MessageBoxIcon.Error); // retour sur le champ erron textSalaire.Focus(); // slection du texte du champ de saisie textSalaire.SelectAll(); // retour l'interface de saisie return; } // le salaire est correct - on peut calculer l'impt labelImpot.Text = Metier.CalculerImpot(radioButtonOui.Checked, (int)numericUpDownEnfants.Value, salaire).ToString(); } private void buttonQuitter_Click(object sender, System.EventArgs e) { Environment.Exit(0); } private void buttonEffacer_Click(object sender, System.EventArgs e) { // raz formulaire labelImpot.Text = ""; numericUpDownEnfants.Value = 0; textSalaire.Text = ""; radioButtonNon.Checked = true; } private void textSalaire_TextChanged(object sender, EventArgs e) { // tat bouton [Calculer] buttonCalculer.Enabled=textSalaire.Text.Trim()!=""; }

Nous ne commentons que les parties importantes :


ligne [8] : la proprit publique Metier qui permet la classe de lancement [Program.cs] d'injecter dans [Form1] une rfrence sur la couche [metier]. ligne [14] : la procdure de calcul de l'impt lignes 15-27 : vrification de la validit du salaire (un nombre entier >=0). ligne 29 : calcul de l'impt l'aide de la mthode [CalculerImpot] de la couche [metier]. On notera la simplicit de cette opration obtenue grce l'encapsulation de la couche [metier] dans une DLL.

5.6.4

Le formulaire [Form2]

En mode [conception] le formulaire [Form2] est le suivant :

Interfaces graphiques

206

Les contrles sont les suivants n type 1 TextBox nom textBoxErreur rle Multiline=True, Scrollbars=Both

Le code de la classe est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System.Windows.Forms; namespace Chap5 { public partial class Form2 : Form { // msg d'erreur public string MsgErreur { private get; set; } public Form2() { InitializeComponent(); } private void Form2_Load(object sender, System.EventArgs e) { // on affiche le msg d'erreur textBoxErreur.Text = MsgErreur; // on dslectionne tout le texte textBoxErreur.Select(0, 0); }

} }

ligne 6 : la proprit publique MsgErreur qui permet la classe de lancement [Program.cs] d'injecter dans [Form2] le message d'erreur afficher. Ce message est affich lors du traitement de l'vnement Load, lignes 12-16. ligne 14 : le message d'erreur est mis dans le TextBox ligne 16 : on enlve la slection qui a lieu lors de l'opration prcdente. [TextBox].Select(dbut,longueur) slectionne (mise en surbrillance) longueur caractres partir du caractre n dbut. [TextBox].Select(0,0) revient dslectionner tout texte.

5.6.5

Conclusion

Revenons sur l'architecture trois couches utilise :

Interfaces graphiques

207

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL 3 2 SPRING

Couche d'accs aux donnes [dao] DLL 1

Donnes

Cette architecture nous a permis de substituer une implmentation graphique l'implmentation console de la couche [ui] existante, sans rien changer aux couches [metier] et [dao]. Nous avons pu nous concentrer sur la couche [ui] sans nous inquiter des impacts possibles sur les autres couches. C'est l le principal intrt des architectures 3 couches. Nous en verrons un autre exemple plus loin, lorsque la couche [dao] qui exploite actuellement les donnes d'un fichier texte, sera remplace par une couche [dao] exploitant les donnes d'une base de donnes. Nous verrons que cela se fera sans impact sur les couches [ui] et [metier].

Interfaces graphiques

208

Evnements utilisateur

Nous avons dans le chapitre prcdent abord la notion d'vnements lis des composants de formulaire. Nous voyons maintenant comment crer des vnements dans nos propres classes.

6.1

Objets delegate prdfinis

La notion d'objet delegate a t rencontre dans le chapitre prcdent mais elle avait t alors survole. Lorsque nous avions regard comment les gestionnaires des vnements des composants d'un formulaire taient dclars, nous avions rencontr du code similaire au suivant :
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

o buttonAfficher tait un composant de type [Button]. Cette classe a un champ Click dfini comme suit : 1 4

2 5

[1] : la classe [Button] [2] : ses vnements [3,4] : l'vnement Click [5] : la dclaration de l'vnement [Control.Click] [4]. EventHandler est un prototype (un modle ) de mthode appel delegate. event est un mot cl qui restreint les fonctionnalits du delegate EventHandler : un objet delegate a des fonctionnalits plus riches qu'un objet event.

Le delegate EventHandler est dfini comme suit :

Evnements utilisateur

209

Le delegate EventHandler dsigne un modle de mthode : ayant pour 1er paramtre un type Object ayant pour 2ime paramtre un type EventArgs ne rendant aucun rsultat Une mthode correspondant au modle dfini par EventHandler pourrait tre la suivante :
private void buttonAfficher_Click(object sender, EventArgs e);

Pour crer un objet de type EventHandler, on procde comme suit :


EventHandler evtHandler=new EventHandler(mthode correspondant au prototype EventHandler); dfini par le type

On pourra ainsi crire :


EventHandler evtHandler=new EventHandler(buttonAfficher_Click);

Une variable de type delegate est en fait une liste de rfrences sur des mthodes correspondant au modle du delegate. Pour ajouter une nouvelle mthode M la variable evtHandler ci-dessus, on utilise la syntaxe :
evtHandler+=new EvtHandler(M);

La notation += peut tre utilise mme si evtHandler est une liste vide. L'instruction :
this.buttonAfficher.Click += new System.EventHandler(this.buttonAfficher_Click);

ajoute une mthode de type EventHandler la liste des mthodes de l'vnement buttonAfficher.Click. Lorsque l'vnement Click sur le composant buttonAfficher se produit, VB excute l'instruction :
buttonAfficher.Click(source, evt);

o :

source est le composant de type object l'origine de l'vnement evt de type EventArgs et ne contient pas d'information

Toutes les mthodes de signature void M(object,EventArgs) qui ont t associes l'vnement Click par :
this.buttonAfficher.Click += new System.EventHandler(M);

seront appeles avec les paramtres (source, evt) transmis par VB.

Evnements utilisateur

210

6.2

Dfinir des objets delegate

L'instruction
public delegate int Opration(int n1, int n2);

dfinit un type appel Opration qui reprsente un prototype de fonction acceptant deux entiers et rendant un entier. C'est le mot cl delegate qui fait de Opration une dfinition de prototype de fonction. Une variable op de type Opration aura pour rle d'enregistrer une liste de fonctions correspondant au prototype Opration :
int f1(int,int) int f2(int,int) ... int fn(int,int)

L'enregistrement d'une mthode fi dans la variable op se fait par op=new Opration(fi) ou plus simplement par op=fi. Pour ajouter une mthode fj la liste des fonctions dj enregistres, on crit op+= fj. Pour enlever une mthode fk dj enregistre on crit op-=fk. Si dans notre exemple on crit n=op(n1,n2), l'ensemble des mthodes enregistres dans la variable op seront excutes avec les paramtres n1 et n2. Le rsultat n rcupr sera celui de la dernire mthode excute. Il n'est pas possible d'obtenir les rsultats produits par l'ensemble des mthodes. Pour cette raison, si on enregistre une liste de mthodes dans une fonction dlgue, celles-ci rendent le plus souvent un rsultat de type void. Considrons l'exemple suivant :
1. using System; 2. namespace Chap6 { 3. class Class1 { 4. // dfinition d'un prototype de fonction 5. // accepte 2 entiers en paramtre et rend un entier 6. public delegate int Opration(int n1, int n2); 7. 8. // deux mthodes d'instance correspondant au prototype 9. public int Ajouter(int n1, int n2) { 10. Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")"); 11. return n1 + n2; 12. }//ajouter 13. 14. public int Soustraire(int n1, int n2) { 15. Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")"); 16. return n1 - n2; 17. }//soustraire 18. 19. // une mthode statique correspondant au prototype 20. public static int Augmenter(int n1, int n2) { 21. Console.WriteLine("Augmenter(" + n1 + "," + n2 + ")"); 22. return n1 + 2 * n2; 23. }//augmenter 24. 25. static void Main(string[] args) { 26. 27. // on dfinit un objet de type opration pour y enregistrer des fonctions 28. // on enregistre la fonction statique augmenter 29. Opration op = Augmenter; 30. // on excute le dlgu 31. int n = op(4, 7); 32. Console.WriteLine("n=" + n); 33. 34. // cration d'un objet c1 de type class1 35. Class1 c1 = new Class1(); 36. // on enregistre dans le dlgu la mthode ajouter de c1 37. op = c1.Ajouter; 38. // excution de l'objet dlgu 39. n = op(2, 3); 40. Console.WriteLine("n=" + n); 41. // on enregistre dans le dlgu la mthode soustraire de c1 42. op = c1.Soustraire; 43. n = op(2, 3); 44. Console.WriteLine("n=" + n); 45. //enregistrement de deux fonctions dans le dlgu 46. op = c1.Ajouter; 47. op += c1.Soustraire; 48. // excution de l'objet dlgu

Evnements utilisateur

211

49. 50. 51. 52. 53. 54. 55. } 56. }

op(0, // on op -= // on op(1, }

0); retire une fonction du dlgu c1.Soustraire; excute le dlgu 1);

ligne 3 : dfinit une classe Class1. ligne 6 : dfinition du delegate Opration : un prototype de mthodes acceptant deux paramtres de type int et rendant un rsultat de type int lignes 9-12 : la mthode d'instance Ajouter a la signature du delegate Opration. lignes 14-17 : la mthode d'instance Soustraire a la signature du delegate Opration. lignes 20-23 : la mthode de classe Augmenter a la signature du delegate Opration. ligne 25 : la mthode Main excute ligne 20 : la variable op est de type delegate Opration. Elle contiendra une liste de mthodes ayant la signature du type delegate Opration. On lui affecte une premire rfrence de mthode, celle sur la mthode statique Class1.Augmenter. ligne 31 : le delegate op est excut : ce sont toutes les mthodes rfrences par op qui vont tre excutes. Elles le seront avec les paramtres passs au delegate op. Ici, seule la mthode statique Class1.Augmenter va tre excute. ligne 35 : une instance c1 de la classe Class1 est cre. ligne 37 : la mthode d'instance c1.Ajouter est affecte au delegate op. Augmenter tait une mthode statique, Ajouter est une mthode d'instance. On a voulu montrer que cela n'avait pas d'importance. ligne 39 : le delegate op est excut : la mthode Ajouter va tre excute avec les paramtres passs au delegate op. ligne 42 : on refait de mme avec la mthode d'instance Soustraire. lignes 46-47 : on met les mthodes Ajouter et Soustraire dans le delegate op. ligne 49 : le delegate op est excut : les deux mthodes Ajouter et Soustraire vont tre excutes avec les paramtres passs au delegate op. ligne 51 : la mthode Soustraire est enleve du delegate op. ligne 53 : le delegate op est excut : la mthode restante Ajouter va tre excute.

Les rsultats de l'excution sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. Augmenter(4,7) n=18 Ajouter(2,3) n=5 Soustraire(2,3) n=-1 Ajouter(0,0) Soustraire(0,0) Ajouter(1,1)

6.3

Delegates ou interfaces ?

Les notions de delegates et d'interfaces peuvent sembler assez proches et on peut se demander quelles sont exactement les diffrences entre ces deux notions. Prenons l'exemple suivant proche d'un exemple dj tudi :
1. using System; 2. namespace Chap6 { 3. class Program1 { 4. // dfinition d'un prototype de fonction 5. // accepte 2 entiers en paramtre et rend un entier 6. public delegate int Opration(int n1, int n2); 7. 8. // deux mthodes d'instance correspondant au prototype 9. public static int Ajouter(int n1, int n2) { 10. Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")"); 11. return n1 + n2; 12. }//ajouter 13. 14. public static int Soustraire(int n1, int n2) { 15. Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")"); 16. return n1 - n2; 17. }//soustraire 18. 19. // Excution d'un dlgu 20. public static int Execute(Opration op, int n1, int n2){ 21. return op(n1, n2);

Evnements utilisateur

212

22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. } 39. }

} static void Main(string[] args) { // excution du dlgu Ajouter Console.WriteLine(Execute(Ajouter, 2, 3)); // excution du dlgu Soustraire Console.WriteLine(Execute(Soustraire, 2, 3)); // excution d'un dlgu multicast Opration op = Ajouter; op += Soustraire; Console.WriteLine(Execute(op, 2, 3)); // on retire une fonction du dlgu op -= Soustraire; // on excute le dlgu Console.WriteLine(Execute(op, 2, 3)); }

Ligne 20, la mthode Execute attend une rfrence sur un objet du type delegate Opration dfini ligne 6. Cela permet de passer la mthode Execute, diffrentes mthodes (lignes 26, 28, 32 et 36). Cette proprit de polymorphisme peut tre obtenue galement avec une interface :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. using System; namespace Chap6 { // interface IOperation public interface IOperation { int operation(int n1, int n2); } // classe Ajouter public class Ajouter : IOperation { public int operation(int n1, int n2) { Console.WriteLine("Ajouter(" + n1 + "," + n2 + ")"); return n1 + n2; } } // classe Soustraire public class Soustraire : IOperation { public int operation(int n1, int n2) { Console.WriteLine("Soustraire(" + n1 + "," + n2 + ")"); return n1 - n2; } } // classe de test public static class Program2 { // Excution de la mthode unique de l'interface IOperation public static int Execute(IOperation op, int n1, int n2) { return op.operation(n1, n2); } public static void Main() { // excution du dlgu Ajouter Console.WriteLine(Execute(new Ajouter(), 2, 3)); // excution du dlgu Soustraire Console.WriteLine(Execute(new Soustraire(), 2, 3)); } } }

lignes 6-8 : l'interface [IOperation] dfinit une mthode operation. lignes 11-16 et 19-24 : les classes [Ajouter] et [Soustraire] implmentent l'interface [IOperation]. lignes 29-31 : la mthode Execute dont le 1er paramtre est du type de l'interface IOperation. La mthode Execute va recevoir successivement comme 1er paramtre, une instance de la classe Ajouter puis une instance de la classe Soustraire.

On retrouve bien l'aspect polymorphique qu'avait le paramtre de type delegate de l'exemple prcdent. Les deux exemples montrent en mme temps des diffrences entre ces deux notions. Les types delegate et interface sont interchangeables

Evnements utilisateur

213

si l'interface n'a qu'une mthode. En effet, le type delegate est une enveloppe pour une unique mthode alors que l'interface peut, elle, dfinir plusieurs mthodes. si l'aspect multicast du delegate n'est pas utilis. Cette notion de multicast n'existe en effet pas dans l'interface.

Si ces deux conditions sont vrifies, alors on a le choix entre les deux signatures suivantes pour la mthode Execute :
1. 2. int Execute(IOperation op, int n1, int n2) int Execute(Opration op, int n1, int n2)

La seconde qui utilise le delegate peut se montrer plus souple d'utilisation. En effet dans la premire signature, le premier paramtre de la mthode doit implmenter l'interface IOperation. Cela oblige crer une classe pour y dfinir la mthode appele tre passe en premier paramtre la mthode Execute. Dans la seconde signature, toute mthode existante ayant la bonne signature fait l'affaire. Il n'y a pas de construction supplmentaire faire.

6.4

Gestion d'vnements

Les objets delegate peuvent servir dfinir des vnements. Une classe C1 peut dfinir un vnement evt de la faon suivante :

un type delegate est dfini dans ou en-dehors de la classe C1 :


delegate TResult Evt(T1 param1, T2 param2, ...);

la classe C1 dfinit un champ de type delegate Evt :


public Evt Evt1;

lorsque une instance c1 de la classe C1 voudra signaler un vnement, elle excutera son delegate Evt1 en lui passant les paramtres dfinis par le delegate Evt. Toutes les mthodes enregistres dans le delegate Evt1 seront alors excutes avec ces paramtres. On peut dire qu'elles ont t averties de l'vnement Evt1. si un objet c2 utilisant un objet c1 veut tre averti de l'occurrence de l'vnement Evt1 sur l'objet c1, il enregistrera l'une de ses mthodes c2.M dans l'objet dlgu c1.Evt1 de l'objet c1. Ainsi sa mthode c2.M sera excute chaque fois que l'vnement Evt1 se produira sur l'objet c1. Il pourra galement de dsinscrire lorsqu'il ne voudra plus tre averti de l'vnement. comme l'objet dlgu c1.Evt1 peut enregistrer plusieurs mthodes, diffrents objets ci pourront s'enregistrer auprs du dlgu c1.Evt1 pour tre prvenus de l'vnement Evt1 sur c1.

Dans ce scnario, on a : une classe qui signale un vnement des classes qui sont averties de cet vnement. On dit qu'elles souscrivent l'vnement ou s'abonnent l'vnement. un type delegate qui dfinit la signature des mthodes qui seront averties de l'vnement Le framework .NET dfinit :

une signature standard du delegate d'un vnement


public delegate void MyEventHandler(object source, EventArgs evtInfo);

source : l'objet qui a signal l'vnement evtInfo : un objet de type EventArgs ou driv qui apporte des informations sur l'vnement le nom du delegate doit tre termin par EventHandler

une faon standard pour dclarer un vnement de type MyEventHandler dans une classe :
1. 2. 3. 4. public Class C1{ public event MyEventHandler Evt1; ... }

Le champ Evt1 est de type delegate. Le mot cl event est l pour restreindre les oprations qu'on peut faire sur lui : de l'extrieur de la classe C1, seules les oprations += et -= sont possibles. Cela empche la suppression (par erreur du dveloppeur par exemple) des mthodes abonnes l'vnement. On peut simplement s'abonner (+=) ou se dsabonner (-=) de l'vnement.

Evnements utilisateur

214

seule une instance de type C1 peut excuter l'appel Evt1(source,evtInfo) qui dclenche l'excution des mthodes abonnes l'vnement Evt1.

Le framework .NET fournit une mthode gnrique satisfaisant la signature du delegate d'un vnement :
public delegate void EventHandler<TEventArgs>(object source, TEventArgs evtInfo) where TEventArgs : EventArgs

le delegate EventHandler utilise le type gnrique TEventArgs qui est le type de son 2ime paramtre le type TEventArgs doit driver du type EventsArgs (where TEventArgs : EventArgs)

Avec ce delegate gnrique, la dclaration d'un vnement X dans la classe C suivra le schma conseill suivant : dfinir un type XEventArgs driv de EventArgs pour encapsuler les informations sur l'vnement X dfinir dans la classe C un vnement de type EventHandler<XEventArgs>. dfinir dans la classe C une mthode protge destine "publier" l'vnement X aux abonns.
protected void OnXHandler(XEventArgs e);

Considrons l'exemple suivant : une classe Emetteur encapsule une temprature. Cette temprature est observe. Lorsque cette temprature dpasse un certain seuil, un vnement doit tre lanc. Nous appellerons cet vnement TemperatureTropHaute. Les informations sur cet vnement seront encapsules dans un type TemperatureTropHauteEventArgs. une classe Souscripteur s'abonne l'vnement prcdent. Lorsqu'elle est avertie de l'vnement, elle affiche un message sur la console. un programme console cre un metteur et deux abonns. Il saisit les tempratures au clavier et les enregistre dans une instance Emetteur. Si celle-ci est trop haute, l'instance Emetteur publie l'vnement TemperatureTropHaute. Pour se conformer la mthode conseille de gestion des vnements, nous dfinissons tout d'abord le type TemperatureTropHauteEventArgs pour encapsuler les informations sur l'vnement :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. using System; namespace Chap6 { public class TemperatureTropHauteEventArgs:EventArgs { // temprature lors de l'vt public decimal Temperature { get; set; } // constructeurs public TemperatureTropHauteEventArgs() { } public TemperatureTropHauteEventArgs(decimal temperature) { Temperature = temperature; } } }

ligne 6 : l'information encapsule par la classe TemperatureTropHauteEventArgs est la temprature qui a provoqu l'vnement TemperatureTropHaute.

La classe Emetteur est la suivante :


1. using System; 2. 3. namespace Chap6 { 4. public class Emetteur { 5. static decimal SEUIL = 19; 6. 7. // temprature observe 8. private decimal temperature; 9. // nom de la source 10. public string Nom { get; set; } 11. // vt signal 12. public event EventHandler<TemperatureTropHauteEventArgs> TemperatureTropHaute; 13. 14. // lecture / criture temprature 15. public decimal Temperature { 16. get { 17. return temperature; 18. } 19. set { 20. temperature = value; 21. if (temperature > SEUIL) { 22. // on signale l'vnement aux abonns

Evnements utilisateur

215

23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. } 34. }

OnTemperatureTropHaute(new TemperatureTropHauteEventArgs(temperature)); } } // signalement d'un vt protected virtual void OnTemperatureTropHaute(TemperatureTropHauteEventArgs evt) { // mission de l'vt TemperatureTropHaute aux abonns TemperatureTropHaute(this, evt); } }

ligne 5 : le seuil de temprature au-del duquel l'vnement TemperatureTropHaute sera publi. ligne 10 : l'metteur a un nom pour tre identifi ligne 12 : l'vnement TemperatureTropHaute. lignes 15-26 : la mthode get qui rend la temprature et la mthode set qui l'enregistre. C'est la mthode set qui fait publier l'vnement TemperatureTropHaute si la temprature enregistrer dpasse le seuil de la ligne 5. Elle fait publier l'vnement par la mthode OnTemperatureTropHauteHandler de la ligne 29 en lui passant pour paramtre un objet TemperatureTropHauteEventArgs dans lequel on a enregistr la temprature qui a dpass le seuil. lignes 29-32 : l'vnement TemperatureTropHaute est publi avec pour 1er paramtre l'metteur lui-mme et pour second paramtre l'objet TemperatureTropHauteEventArgs reu en paramtre.

La classe Souscripteur qui va s'abonner l'vnement TemperatureTropHaute est la suivante :


1. using System; 2. 3. namespace Chap6 { 4. public class Souscripteur { 5. // nom 6. public string Nom { get; set; } 7. 8. // gestionnaire de l'vt TemperatureTropHaute 9. public void EvtTemperatureTropHaute(object source, TemperatureTropHauteEventArgs e) { 10. // affichage console oprateur 11. Console.WriteLine("Souscripteur [{0}] : la source [{1}] a signal une temprature trop haute : [{2}]", Nom, ((Emetteur)source).Nom, e.Temperature); 12. } 13. } 14. }

ligne 6 : chaque souscripteur est identifi par un nom. lignes 9-12 : la mthode qui sera associe l'vnement TemperatureTropHaute. Elle a la signature du type delegate EventHandler<TEventArgs> qu'un gestionnaire d'vnement doit avoir. La mthode affiche sur la console : le nom du souscripteur qui affiche le message, le nom de l'metteur qui a signal l'vnement, la temprature qui a dclench ce dernier. l'abonnement l'vnement TemperatureTropHaute d'un objet Emetteur n'est pas fait dans la classe Souscripteur. Il sera fait par une classe externe.

Le programme [Program.cs] lie tous ces lments entre-eux :


1. using System; 2. namespace Chap6 { 3. class Program { 4. static void Main(string[] args) { 5. // cration d'un metteur d'evts 6. Emetteur e1 = new Emetteur() { Nom = "e" }; 7. // cration d'un tableau de 2 souscripteurs 8. Souscripteur[] souscripteurs = new Souscripteur[2]; 9. for (int i = 0; i < souscripteurs.Length; i++) { 10. // cration souscripteur 11. souscripteurs[i] = new Souscripteur() { Nom = "s" + i }; 12. // on l'abonne l'vt TemperatureTropHaute de e1 13. e1.TemperatureTropHaute += souscripteurs[i].EvtTemperatureTropHaute; 14. } 15. // on lit les tempratures au clavier 16. decimal temperature; 17. Console.Write("Temprature (rien pour arrter) : "); 18. string saisie = Console.ReadLine().Trim(); 19. // tant que la ligne saisie est non vide 20. while (saisie != "") { 21. // la saisie est-elle un nombre dcimal ? 22. if (decimal.TryParse(saisie, out temperature)) {

Evnements utilisateur

216

23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. } 35. }

// temprature correcte - on l'enregistre e1.Temperature = temperature; } else { // on signale l'erreur Console.WriteLine("Temprature incorrecte"); } // nouvelle saisie Console.Write("Temprature (rien pour arrter) : "); saisie = Console.ReadLine().Trim(); }//while

ligne 6 : cration de l'metteur lignes 8-14 : cration de deux souscripteurs qu'on abonne l'vnement TemperatureTropHaute de l'metteur. lignes 20-32 : boucle de saisie des tempratures au clavier ligne 24 : si la temprature saisie est correcte, elle est transmise l'objet Emetteur e1 qui dclenchera l'vnement TemperatureTropHaute si la temprature est suprieure 19 C.

Les rsultats de l'excution sont les suivants :


1. 2. 3. 4. Temprature (rien Temprature (rien Souscripteur [s0] Souscripteur [s1] pour pour : la : la arrter) : arrter) : source [e] source [e] 17 21 a signal une temprature trop haute : [21] a signal une temprature trop haute : [21]

Evnements utilisateur

217

7
7.1

Accs aux bases de donnes


Connecteur ADO.NET

Reprenons l'architecture en couches utilise diverses reprises

utilisateur

Couche ui [ui]

Couche mtier [metier]

Couche d'accs aux donnes [dao] 1

Donnes

SPRING Dans les exemples tudis, la couche [dao] a pour l'instant exploit deux types de sources de donnes : des donnes places en dur dans le code des donnes provenant de fichiers texte Nous tudions dans ce chapitre le cas o les donnes proviennent d'une base de donnes. L'architecture 3 couches volue alors vers une architecture multi-couches. Il en existe diverses. Nous allons tudier les concepts de base avec la suivante :

utilisateur

Couche d'accs aux donnes [dao] 1 SPRING

Connecteur [ADO.NET] 2

SGBD 3

BD 4

Dans le schma ci-dessus, la couche [dao] [1] dialogue avec le SGBD [3] au travers d'une bibliothque de classes propre au SGBD utilis et livre avec lui. Cette couche implmente des fonctionnalits standard runies sous le vocable ADO (Active X Data Objects). On appelle une telle couche, un provider (fournisseur d'accs une base de donnes ici) ou encore connecteur. La plupart des SGBD disposent dsormais d'un connecteur ADO.NET, ce qui n'tait pas le cas aux dbuts de la plate-forme .NET. Les connecteurs .NET n'offrent pas une interface standard la couche [dao], aussi celle-ci a-t-elle dans son code le nom des classes du connecteur. Si on change de SGBD, on change de connecteur et de classes et il faut alors changer la couche [dao]. C'est la fois une architecture performante parce le connecteur .NET ayant t crit pour un SGBD particulier sait utiliser au mieux celui-ci et rigide car changer de SGBD implique de changer la couche [dao]. Ce deuxime argument est relativiser : les entreprises ne changent pas de SGBD trs souvent. Par ailleurs, nous verrons ultrieurement que depuis la version 2.0 de .NET, il existe un connecteur gnrique qui amne de la souplesse sans sacrifier la performance.

7.2

Les deux modes d'exploitation d'une source de donnes

La plate-forme .NET permet l'exploitation d'une source de donnes de deux manires diffrentes : 1. 2. mode connect mode dconnect

En mode connect, l'application 1. ouvre une connexion avec la source de donnes 2. travaille avec la source de donnes en lecture/criture 3. ferme la connexion En mode dconnect, l'application 1. ouvre une connexion avec la source de donnes 2. obtient une copie mmoire de tout ou partie des donnes de la source 3. ferme la connexion

Accs aux bases de donnes

218

4. 5.

travaille avec la copie mmoire des donnes en lecture/criture lorsque le travail est fini, ouvre une connexion, envoie les donnes modifies la source de donnes pour qu'elle les prenne en compte, ferme la connexion

Nous n'tudions ici que le mode connect.

7.3

Les concepts de base de l'exploitation d'une base de donnes

Nous allons exposer les principaux concepts d'utilisation d'une base de donnes avec une base de donnes SQL Server Compact 3.5. Ce SGBD est livr avec Visual Studio Express. C'est un SGBD lger qui ne sait grer qu'un utilisateur la fois. Il est cependant suffisant pour introduire la programmation avec les bases de donnes. Ultrieurement, nous prsenterons d'autres SGBD. L'architecture utilise sera la suivante : SGBD SqlServer CE 3

utilisateur

Application [console] 1

Connecteur [ADO.NET] 2

BD 4

Une application console [1] exploitera une base de donnes de type SqlServer Compact [3,4] via le connecteur Ado.Net de ce SGBD [2].

7.3.1

La base de donnes exemple

Nous allons construire la base de donnes directement dans Visual studio Express. Pour cela, nous crons un nouveau projet de type console.

[1] : le projet [2] : on ouvre une vue "Explorateur de bases de donnes" [3] : on cre une nouvelle connexion

Accs aux bases de donnes

219

9 6 4 11 12 7 13 10

[4] : on slectionne le type du SGBD [5,6] : on choisit le SGBD SQL Server Compact [7] : on cre la base de donnes [8] : une base de donnes SQL Server Compact est encapsule dans un unique fichier de suffixe .sdf. On indique o la crer, ici dans le dossier du projet C#. [9] : on a donn le nom [dbarticles.sdf] la nouvelle base [10] : on slectionne la langue franaise. Cela a une consquence sur les oprations de tri. [11,12] : la base de donnes peut tre protge par un mot de passe. Ici "dbarticles". [13] : on valide la page de renseignements. La base va tre physiquement cre :

Accs aux bases de donnes

220

17

19 14

20 15 18 16

[14] : le nom de la base qui vient d'tre cre [15] : on coche l'option "Save my password" afin de ne pas avoir le retaper chaque fois [16] : on vrifie la connexion [17] : tout va bien [18] : on valide la page d'informations [19] : la connexion apparat dans l'explorateur de bases de donnes [20] : pour l'instant la base est sans tables. On en cre une. Un article aura les champs suivants : id : un identifiant unique - cl primaire nom : nom de l'article - unique prix : prix de l'article stockactuel : son stock actuel stockminimum : le stock minimum en-dea duquel il faut rapprovisionner larticle 23 21 22

[21] : le champ [id] est de type entier et est cl primaire [22] de la table. [23] : cette cl primaire est de type Identity. Cette notion propre aux SGBD SQL Server indique que la cl primaire sera gnre par le SGBD lui-mme. Ici la cl primaire sera un nombre entier commenant 1 et incrment de 1 chaque nouvelle cl.

Accs aux bases de donnes

221

26

25 27 24

[24] : les autres champs sont crs. On notera que le champ [nom] a une contrainte d'unicit [25]. [26] : on donne un nom la table [27] : aprs avoir valid la structure de la table, celle-ci apparat dans la base.

29

30 28

[28] : on demande voir le contenu de la table [29] : elle est vide pour l'instant [30] : on la remplit avec quelques donnes. Une ligne est valide ds qu'on passe la saisie de la ligne suivante. Le champ [id] n'est pas saisi : il est gnr automatiquement lorsque la ligne est valide.

Il nous reste configurer le projet pour que cette base qui est actuellement la racine du projet soit recopie automatiquement dans le dossier d'excution du projet : 1 3

[1] : on demande voir tous les fichiers [2] : la base [dbarticles.sdf] apparat [3] : on l'inclut dans le projet

Accs aux bases de donnes

222

7 8 4

6 9

10 5

[4] : l'opration d'ajout d'une source de donnes dans un projet lance un assistant dont nous n'avons pas besoin ici [5]. [6] : la base fait maintenant partie du projet. On revient en mode normal [7]. [8] : le projet avec sa base [9] : dans les proprits de la base, on peut voir [10] que celle-ci sera automatiquement recopie dans le dossier d'excution du projet. C'est l que le programme que nous allons crire, ira la chercher.

Maintenant que nous avons une base de donnes disponible, nous allons pouvoir l'exploiter. Auparavant, nous faisons quelques rappels SQL.

7.3.2

Les quatre commandes de base du langage SQL

SQL (Structured Language Query) est un langage, partiellement normalis, d'interrogation et de mise jour des bases de donnes. Tous les SGBD respectent la partie normalise de SQL mais ajoutent au langage des extensions propritaires qui exploitent certaines particularits du SGBD. Nous en avons dj rencontr deux exemples : la gnration automatique des cls primaires et les types autoriss pour les colonnes d'une table sont souvent dpendants du SGBD. Les quatre commandes de base du langage SQL que nous prsentons sont normalises et acceptes par tous les SGBD :
select col1, col2,... from table1, table2,... where condition order by expression ...

La requte qui permet d'obtenir les donnes contenues dans une base. Seuls les mots cls de la premire ligne sont obligatoires, les autres sont facultatifs. Il existe dautres mots cls non prsents ici. 1. 2. 3. 4. Une jointure est faite avec toutes les tables qui sont derrire le mot cl from Seules les colonnes qui sont derrire le mot cl select sont conserves Seules les lignes vrifiant la condition du mot cl where sont conserves Les lignes rsultantes ordonnes selon lexpression du mot cl order by forment le rsultat de la requte. Ce rsultat est une table.

insert into table(col1,col2, ...) values (val1,val2, ...) update table set col1=val1, col2=val2 where condition delete from table where condition

Insre une ligne dans table. (col1, col2, ...) prcise les colonnes de la ligne initialiser avec les valeurs (val1, val2, ...). Met jour les lignes de table vrifiant condition (toutes les lignes si pas de where). Pour ces lignes, la colonne coli reoit la valeur vali Supprime toutes les lignes de table vrifiant condition

Nous allons crire une application console permettant d'mettre des ordres SQL sur la base [dbarticles] que nous avons cre prcdemment. Voici un exemple d'excution. Le lecteur est invit comprendre les ordres SQL mis et leurs rsultats.
1. Chane de connexion la base : [Data Source=| DataDirectory|\dbarticles.sdf;Password=dbarticles;Persist Security Info=True]

Accs aux bases de donnes

223

2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16.

Requte SQL (rien pour arrter) : select id,nom,prix,stockactuel,stockminimum from articles -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------1 2 3 4 5 6 vlo 500 10 5 pompe 10 10 2 arc 600 4 1 flches - lot de 6 100 12 20 combinaison de plonge 300 8 2 bouteilles d'oxygne 120 10 5 into articles(nom,prix,stockactuel,stockminimum)

Requte SQL (rien pour arrter) : insert values('x',100,10,1) 17. Il y a eu 1 ligne(s) modifie(s) 18. 19. Requte SQL (rien pour arrter) : select 20. 21. -----------------------------------22. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM 23. -----------------------------------24. 25. 1 vlo 500 10 5 26. ... 27. 6 bouteilles d'oxygne 120 10 5 28. 9 x 100 10 1 29. 30. Requte SQL (rien pour arrter) : update 31. Il y a eu 1 ligne(s) modifie(s) 32. 33. Requte SQL (rien pour arrter) : select 34. 35. -----------------------------------36. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM 37. -----------------------------------38. 39. 1 vlo 500 10 5 40. ... 41. 6 bouteilles d'oxygne 120 10 5 42. 9 x 110 10 1 43. 44. Requte SQL (rien pour arrter) : delete 45. Il y a eu 1 ligne(s) modifie(s) 46. 47. Requte SQL (rien pour arrter) : select 48. 49. -----------------------------------50. ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM 51. -----------------------------------52. 53. 1 vlo 500 10 5 54. ... 55. 6 bouteilles d'oxygne 120 10 5

id,nom,prix,stockactuel,stockminimum from articles

articles set prix=prix*1.1 where id=9 id,nom,prix,stockactuel,stockminimum from articles

from articles where id=9 id,nom,prix,stockactuel,stockminimum from articles

ligne 1 : la chane dite de connexion : elle contient tous les paramtres permettant de se connecter la base de donnes. ligne 3 : on demande le contenu de la table [articles] ligne 16 : on insre une nouvelle ligne. On notera que le champ id n'est pas initialis dans cette opration car c'est le SGBD qui va gnrer la valeur de ce champ. ligne 19 : vrification. Ligne 28, la ligne a bien t ajoute. ligne 30 : on augmente de 10% le prix de l'article qui vient d'tre ajout. ligne 33 : on vrifie ligne 42 : l'augmentation du prix a bien eu lieu ligne 44 : on supprime l'article qu'on a ajout prcdemment ligne 47 : on vrifie lignes 53-55 : l'article n'est plus l.

7.3.3

Les interfaces de base d'ADO.NET pour le mode connect

Revenons au schma d'une application exploitant une base de donnes au travers d'un connecteur ADO.NET :

Accs aux bases de donnes

224

utilisateur

Couche d'accs aux donnes [dao] 1

Connecteur [ADO.NET] 2

SGBD 3

BD 4

En mode connect, l'application : 1. ouvre une connexion avec la source de donnes 2. travaille avec la source de donnes en lecture/criture 3. ferme la connexion Trois interfaces ADO.NET sont principalement concernes par ces oprations :

IDbConnection qui encapsule les proprits et mthodes de la connexion. IDbCommand qui encapsule les proprits et mthodes de la commande SQL excute. IDataReader qui encapsule les proprits et mthodes du rsultat d'un ordre SQL Select.

L'interface IDbConnection Sert grer la connexion avec la base de donnes. Les mthodes M et proprits P de cette interface que nous utiliserons seront les suivantes : Nom
ConnectionString Open Close BeginTransaction State

Type P M M M P

Rle chane de connexion la base. Elle prcise tous les paramtres ncessaires l'tablissement de la connexion avec une base prcise. ouvre la connexion avec la base dfinie par ConnectionString ferme la connexion dmarre une transaction. tat de la connexion : ConnectionState.Closed, ConnectionState.Open, ConnectionState.Executing, ConnectionState.Fetching, ConnectionState.Broken ConnectionState.Connecting,

Si Connection est une classe implmentant l'interface IDbConnection, l'ouverture de la connexion peut se faire comme suit :
1. 2. 3. IDbConnection connexion=new Connection(); connexion.ConnectionString=...; connexion.Open();

L'interface IDbCommand Sert excuter un ordre SQL ou une procdure stocke. Les mthodes M et proprits P de cette interface que nous utiliserons seront les suivantes : Nom
CommandType

Type P

Rle indique ce qu'il faut excuter - prend ses valeurs dans une numration : - CommandType.Text : excute l'ordre SQL dfini dans la proprit CommandText. C'est la valeur par dfaut. - CommandType.StoredProcedure : excute une procdure stocke dans la base - le texte de l'ordre SQL excuter si CommandType= CommandType.Text - le nom de la procdure stocke excuter si CommandType= CommandType.StoredProcedure la connexion IDbConnection utiliser pour excuter l'ordre SQL la transaction IDbTransaction dans laquelle excuter l'ordre SQL la liste des paramtres d'un ordre SQL paramtr. L'ordre update articles set prix=prix*1.1 where id=@id a le paramtre @id. pour excuter un ordre SQL Select. On obtient un objet IDataReader reprsentant le rsultat du Select.

CommandText Connection Transaction Parameters ExecuteReader

P P P P M

Accs aux bases de donnes

225

Nom
ExecuteNonQuery ExecuteScalar CreateParameter Prepare

Type M M M M

Rle pour excuter un ordre SQL Update, Insert, Delete. On obtient le nombre de lignes affectes par l'opration (mises jour, insres, dtruites). pour excuter un ordre SQL Select ne rendant qu'un unique rsultat comme dans : select count(*) from articles. pour crer les paramtres IDbParameter d'un ordre SQL paramtr. permet d'optimiser l'excution d'une requte paramtre lorsqu'elle est excute de multiples fois avec des paramtres diffrents.

Si Command est une classe implmentant l'interface IDbCommand, l'excution d'un ordre SQL sans transaction aura la forme suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. // ouverture connexion IDbConnection connexion=... connexion.Open(); // prparation commande IDbCommand commande=new Command(); commande.Connection=connexion; // excution ordre select commande.CommandText="select ..."; IDbDataReader reader=commande.ExecuteReader(); ... // excution ordre update, insert, delete commande.CommandText="insert ..."; int nbLignesInsres=commande.ExecuteNonQuery(); ... // fermeture connexion connexion.Close();

L'interface IDataReader Sert encapsuler les rsultats d'un ordre SQL Select. Un objet IDataReader reprsente une table avec des lignes et des colonnes, qu'on exploite squentiellement : d'abord la 1re ligne, puis la seconde, .... Les mthodes M et proprits P de cette interface que nous utiliserons seront les suivantes : Nom
FieldCount GetName Item Read Close GetBoolean

Type P M P M M M le nombre de colonnes de la table IDataReader

Rle GetName(i) rend le nom de la colonne n i de la table IDataReader. Item[i] reprsente la colonne n i de la ligne courante de la table IDataReader. passe la ligne suivante de la table IDataReader. Rend le boolen True si la lecture a pu se faire, False sinon. ferme la table IDataReader. GetBoolean(i) : rend la valeur boolenne de la colonne n i de la ligne courante de la table IDataReader. Les autres mthodes analogues sont les suivantes : GetDateTime, GetDecimal, GetDouble, GetFloat, GetInt16, GetInt32, GetInt64, GetString. Getvalue(i) : rend la valeur de la colonne n i de la ligne courante de la table IDataReader en tant que type object. IsDBNull(i) rend True si la colonne n i de la ligne courante de la table IDataReader n'a pas de valeur ce qui est symbolis par la valeur SQL NULL.

Getvalue IsDBNull

M M

L'exploitation d'un objet IDataReader ressemble souvent ce qui suit :


1. 2. 3. 4. 5. 6. 7. 8. 9. // ouverture connexion IDbConnection connexion=... connexion.Open(); // prparation commande IDbCommand commande=new Command(); commande.Connection=connexion; // excution ordre select commande.CommandText="select ..."; IDataReader reader=commande.ExecuteReader();

Accs aux bases de donnes

226

10. 11. 12. 13. 14. 15. 16. 17. 18.

// exploitation rsultats while(reader.Read()){ // exploiter ligne courante ... } // fermeture reader reader.Close(); // fermeture connexion connexion.Close();

7.3.4

La gestion des erreurs

Revenons sur l'architecture d'une application avec base de donnes :

utilisateur

Couche d'accs aux donnes [dao] 1

Connecteur [ADO.NET] 2

SGBD 3

BD 4

La couche [dao] peut rencontrer de nombreuses erreurs lors de l'exploitation de la base de donnes. Celles-ci vont tre remontes en tant qu'exceptions lances par le connecteur ADO.NET. Le code de la couche [dao] doit les grer. Toute opration avec la base de donnes doit se faire dans un try / catch / finally pour intercepter et grer une ventuelle exception et librer les ressources qui doivent l'tre. Ainsi le code vu plus haut pour exploiter le rsultat d'un ordre Select devient le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. // initialisation connexion IDbConnection connexion=... // exploitation connexion try{ // ouverture connexion.Open(); // prparation commande IDbCommand commande=new Command(); commande.Connection=connexion; // excution ordre select commande.CommandText="select ..."; IDbDataReader reader=commande.ExecuteReader(); // exploitation rsultats try{ while(reader.Read()){ // exploiter ligne courante ... }finally{ // fermeture reader reader.Close(); } }catch(Exception ex){ // gestion exception ... }finally{ // fermeture connexion connexion.Close(); } ...

Quoiqu'il arrive, les objets IDataReader et IDbConnection doivent tre ferms. C'est pourquoi cette fermeture est faite dans les clauses finally. La fermeture de la connexion et celle du de l'objet IDataReader peuvent tre automatises avec une clause using :
1. // exploitation connexion 2. try{ 3. using(IDbConnection connexion=...){ 4. // ouverture 5. connexion.Open(); 6. // prparation commande 7. IDbCommand commande=new Command(); 8. commande.Connection=connexion; 9. // excution ordre select 10. commande.CommandText="select ...";

Accs aux bases de donnes

227

11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22.

using(IDbDataReader reader=commande.ExecuteReader()){ // exploitation rsultats while(reader.Read()){ // exploiter ligne courante ... }// using IData }//using IDbConnection }catch(Exception ex){ // gestion exception ... } ..

Ligne 3, la clause using nous assure que la connexion ouverte dans le bloc using(...){...} sera ferme en-dehors de celuici, ceci quelque soit la faon dont on sort du bloc : normalement ou par l'arrive d'une exception. On conomise un finally, mais l'intrt n'est pas dans cette conomie mineure. L'utilisation d'un using vite au dveloppeur de fermer lui-mme la connexion. Or oublier de fermer une connexion peut passer inaperu et "planter" l'application d'une faon qui apparatra alatoire, chaque fois que le SGBD atteindra le nombre maximum de connexions ouvertes qu'il peut supporter. Ligne 11 : on procde de faon analogue pour fermer l'objet IDataReader.

7.3.5

Configuration du projet exemple

Le projet final sera le suivant :

2 1 4 5 6

[1] : le projet aura un fichier de configuration [App.config] [2] : il utilise des classes de deux DLL non rfrences par dfaut et qu'il faut don ajouter aux rfrences du projet : [System.Configuration] pour exploiter le fichier de configuration [App.config] [System.Data.SqlServerCe] pour exploiter la base de donnes Sql Server Compact [3, 4] : rappellent comment ajouter des rfrences un projet. [5, 6] : rappellent comment ajouter le fichier [App.config] un projet.

Le fichier de configuration [App.config] sera le suivant :


1. 2. 3. 4. 5. 6. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbSqlServerCe" connectionString="Data Source=| DataDirectory|\dbarticles.sdf;Password=dbarticles;" /> </connectionStrings> </configuration>

lignes 3-5 : la balise <connectionStrings> au pluriel dfinit des chanes de connexion des bases de donnes. Une chane de connexion la forme "paramtre1=valeur1;paramtre2=valeur2;...". Elle dfinit tous les paramtres ncessaires l'tablissement d'une connexion avec une base de donnes particulire. Ces chanes de connexion changent avec chaque SGBD. Le site [http://www.connectionstrings.com/] donne la forme de celles-ci pour les principaux SGBD.

Accs aux bases de donnes

228

ligne 4 : dfinit une chane de connexion particulire, ici celle de la base SQL Server Compact dbarticles.sdf que nous avons cre prcdemment : name = nom de la chane de connexion. C'est via ce nom qu'une chane de connexion est rcupre par le programme C# connectionString : la chane de connexion pour une base SQL Server Compact DataSource : dsigne le chemin de la base. La syntaxe |DataDirectory| dsigne le dossier d'excution du projet. Password : le mot de passe de la base. Ce paramtre est absent s'il n'y a pas de mot de passe.

Le code C# pour rcuprer la chane de connexion prcdente est le suivant :


string connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString;

ConfigurationManager est la classe de la DLL [System.Configuration] qui permet d'exploiter le fichier [App.config]. ConnectionsStrings["nom"].ConnectionString : dsigne l'attribut connectionString de la balise < add name="nom" connectionString="..."> de la section <connectionStrings> de [App.config]

Le projet est dsormais configur. Nous tudions maintenant la classe [Program.cs] dont nous avons vu prcdemment un exemple d'excution.

7.3.6

Le programme exemple

Le programme [program.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. using using using using using using System; System.Collections.Generic; System.Data.SqlServerCe; System.Text; System.Text.RegularExpressions; System.Configuration;

namespace Chap7 { class SqlCommands { static void Main(string[] args) { // application console - excute des requtes SQL tapes au clavier // sur une base de donnes dont la chane de connexion est obtenue dans un fichier de configuration // exploitation du fichier de configuration [App.config] string connectionString = null; try { connectionString = ConfigurationManager.ConnectionStrings["dbSqlServerCe"].ConnectionString; } catch (Exception e) { Console.WriteLine("Erreur de configuration : {0}", e.Message); return; } // affichage chane de connexion Console.WriteLine("Chane de connexion la base : [{0}]\n", connectionString); // on construit un dictionnaire des commandes sql acceptes string[] commandesSQL = new string[] { "select", "insert", "update", "delete" }; Dictionary<string, bool> dicoCommandes = new Dictionary<string, bool>(); for (int i = 0; i < commandesSQL.Length; i++) { dicoCommandes.Add(commandesSQL[i], true); } // lecture-excution des commandes SQL tapes au clavier string requte = null; // texte de la requte SQL string[] champs; // les champs de la requte Regex modle = new Regex(@"\s+"); // suite d'espaces // boucle de saisie-excution des commandes SQL tapes au clavier while (true) { // demande de la requte Console.Write("\nRequte SQL (rien pour arrter) : "); requte = Console.ReadLine().Trim().ToLower(); // fini ? if (requte == "") break;

Accs aux bases de donnes

229

47. 48. 49. 50. 51. 52.

// on dcompose la requte en champs champs = modle.Split(requte); // requte valide ? if (champs.Length == 0 || ! dicoCommandes.ContainsKey(champs[0])) { // msg d'erreur Console.WriteLine("Requte invalide. Utilisez select, insert, update, delete ou rien pour arrter"); 53. // requte suivante 54. continue; 55. } 56. // excution de la requte 57. if (champs[0] == "select") { 58. ExecuteSelect(connectionString, requte); 59. } else 60. ExecuteUpdate(connectionString, requte); 61. } 62. } 63. 64. // excution d'une requte de mise jour 65. static void ExecuteUpdate(string connectionString, string requte) { 66. ... 67. } 68. 69. // excution d'une requte Select 70. static void ExecuteSelect(string connectionString, string requte) { 71. .... 72. } 73. } 74. }

lignes 1-6 : les espaces de nom utiliss dans l'application. La gestion d'une base de donnes SQL Server Compact ncessite l'espace de noms [System.Data.SqlServerCe] de la ligne 3. On a l une dpendance sur un espace de noms propritaire un SGBD. On peut en dduire que le programme devra tre modifi si on change de SGBD. ligne 18 : la chane de connexion la base est lue dans le fichier [App.config] et affiche ligne 25. Elle servira pour l'tablissement d'une connexion avec la base de donnes. lignes 28-32 : un dictionnaire mmorisant les noms des quatre ordres SQL autoriss : select, insert, update, delete. lignes 40-62 : la boucle de saisie des ordres SQL taps au clavier et leur excution sur la base de donnes ligne 48 : la ligne tape au clavier est dcompose en champs afin d'en connatre le premier terme qui doit tre : select, insert, update, delete lignes 50-55 : si la requte est invalide, un message d'erreur est affich et on passe la requte suivante. lignes 57-61 : on excute l'ordre SQL saisi. Cette excution prend une forme diffrente selon que l'on a affaire un ordre select ou un ordre insert, update, delete. Dans le premier cas, l'ordre ramne des donnes de la base sans modifier celle-ci, dans le second il la met jour sans ramener des donnes. Dans les deux cas, on dlgue l'excution une mthode qui a besoin de deux paramtres : la chane de connexion qui va lui permettre de se connecter la base l'ordre SQL excuter sur cette connexion

7.3.7

Excution d'une requte SELECT

L'excution d'ordres SQL ncessite les tapes suivantes : 1. 2. 3. 4. Connexion la base de donnes mission des ordres SQL vers la base Traitement des rsultats de l'ordre SQL Fermeture de la connexion

Les tapes 2 et 3 sont ralises de faon rpte, la fermeture de connexion nayant lieu qu la fin de lexploitation de la base. Les connexions ouvertes sont des ressources limites d'un SGBD. Il faut les conomiser. Aussi cherchera-t-on toujours limiter la dure de vie d'une connexion ouverte. Dans l'exemple tudi, la connexion est ferme aprs chaque ordre SQL. Une nouvelle connexion est ouverte pour l'ordre SQL suivant. L'ouverture / fermeture d'une connexion est coteuse. Pour diminuer ce cot, certains SGBD offrent la notion de pools de connexions ouvertes : lors de l'initialisation de l'application, N connexions sont ouvertes et sont affectes au pool. Elles resteront ouvertes jusqu' la fin de l'application. Lorsque l'application ouvre une connexion, elle reoit l'une des N connexions dj ouvertes du pool. Lorsqu'elle ferme la connexion, celle-ci est simplement remise dans le pool. L'intrt de ce systme est qu'il est transparent pour le dveloppeur : le programme n'a pas tre modifi pour utiliser le pool de connexions. La configuration du pool de connexions est dpendant du SGBD.

Accs aux bases de donnes

230

Nous nous intressons tout d'abord l'excution des ordres SQL Select. La mthode ExecuteSelect de notre programme exemple est la suivante :
1. // excution d'une requte Select 2. static void ExecuteSelect(string connectionString, string requte) { 3. // on gre les ventuelles exceptions 4. try { 5. using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) { 6. // ouverture connexion 7. connexion.Open(); 8. // excute sqlCommand avec requte select 9. SqlCeCommand sqlCommand = new SqlCeCommand(requte, connexion); 10. SqlCeDataReader reader= sqlCommand.ExecuteReader(); 11. // affichage rsultats 12. AfficheReader(reader); 13. } 14. } catch (Exception ex) { 15. // msg d'erreur 16. Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); 17. } 18. } 19. 20. // affichage reader 21. static void AfficheReader(IDataReader reader) { 22. ... 23. }


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23.

ligne 2 : la mthode reoit deux paramtres : la chane de connexion [connectionString] qui va lui permettre de se connecter la base l'ordre SQL Select [requte] excuter sur cette connexion ligne 4 : toute opration avec une base de donnes peut gnrer une exception qu'on peut vouloir grer. C'est d'autant plus important ici que les ordres SQL donns par l'utilisateur peuvent tre syntaxiquement errons. Il faut qu'on puisse le lui dire. Tout le code est donc l'intrieur d'un try / catch. ligne 5 : il y a plusieurs choses ici : la connexion avec la base est initialise avec la chane de connexion [connectionString]. Elle n'est pas encore ouverte. Elle le sera ligne 7. la clause using (Ressource) {...} est une facilit syntaxique garantissant la libration de la ressource Ressource, ici une connexion, la sortie du bloc contrl par le using. la connexion est d'un type propritaire : SqlCeConnection, propre au SGBD SQL Server Compact. ligne 7 : la connexion est ouverte. C'est ce moment que les paramtres de la chane de connexion sont utiliss. ligne 9 : un ordre SQL est mis via un objet propritaire SqlCeCommand. La ligne 9 initialise cet objet avec deux informations : la connexion utiliser et l'ordre SQL mettre dessus. L'objet SqlCeCommand sert aussi bien excuter un ordre Select qu'un ordre Update, Insert, Delete. Ses proprits et mthodes ont t prsentes page 225. ligne 10 : un ordre SQL Select est excut via la mthode ExecuteReader de l'objet SqlCeCommand qui rend un objet IDataReader dont a prsent les mthodes et proprits page 226. ligne 12 : l'affichage des rsultats est confie la mthode AfficheReader suivante :
// affichage reader static void AfficheReader(IDataReader reader) { using (reader) { // exploitation des rsultats // -- colonnes StringBuilder ligne = new StringBuilder(); int i; for (i = 0; i < reader.FieldCount - 1; i++) { ligne.Append(reader.GetName(i)).Append(","); } ligne.Append(reader.GetName(i)); Console.WriteLine("\n{0}\n{1}\n{2}\n", "".PadLeft(ligne.Length, '-'), ligne, "".PadLeft(ligne.Length, '-')); // -- donnes while (reader.Read()) { // exploitation ligne courante ligne = new StringBuilder(); for (i = 0; i < reader.FieldCount; i++) { ligne.Append(reader[i].ToString()).Append(" "); } Console.WriteLine(ligne); } } }

Accs aux bases de donnes

231

ligne 2 : la mthode reoit un objet IDataReader. On notera qu'ici nous avons utilis une interface et non une classe spcifique. ligne 3 : la clause using est utilise pour grer de faon automatique la fermeture de l'objet IDataReader. lignes 8-10 : on affiche les noms des colonnes de la table rsultat du Select. Ce sont les colonnes coli de la requte select col1, col2, ... from table ... lignes 14-21 : on parcourt la table des rsultats et on affiche les valeurs de chaque ligne de la table. ligne 18 : on ne connat pas le type de la colonne n i du rsultat parce qu'on ne connat pas la table interroge. On ne peut donc utiliser la syntaxe reader.GetXXX(i) o XXX est le type de la colonne n i, car on ne connat pas ce type. On utilise alors la syntaxe reader.Item[i].ToString() pour avoir la reprsentation de la colonne n i sous forme de chane de caractres. La syntaxe reader.Item[i].ToString() peut tre abrge en reader[i].ToString().

7.3.8

Excution d'un ordre de mise jour : INSERT, UPDATE, DELETE

Le code de la mthode ExecuteUpdate est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. } // excution d'une requte de mise jour static void ExecuteUpdate(string connectionString, string requte) { // on gre les ventuelles exceptions try { using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) { // ouverture connexion connexion.Open(); // excute sqlCommand avec requte de mise jour SqlCeCommand sqlCommand = new SqlCeCommand(requte, connexion); int nbLignes = sqlCommand.ExecuteNonQuery(); // affichage rsultat Console.WriteLine("Il y a eu {0} ligne(s) modifie(s)", nbLignes); } } catch (Exception ex) { // msg d'erreur Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); }

Nous avons dit que l'excution d'un ordre d'interrogation Select ne diffrait de celle d'un ordre de mise jour Update, Insert, Delete que par la mthode de l'objet SqlCeCommand utilise : ExecuteReader pour Select, ExecuteNonQuery pour Update, Insert, Delete. Nous ne commentons que cette dernire mthode dans le code ci-dessus :

ligne 10 : l'ordre Update, Insert, Delete est excut par la mthode ExecuteNonQuery de l'objet SqlCeCommand. Si elle russit, cette mthode rend le nombre de lignes mises jour (update) ou insres (insert) ou dtruites (delete). ligne 12 : ce nombre de lignes est affich l'cran

Le lecteur est invit revoir un exemple d'excution de ce code, page 223.

7.4

Autres connecteurs ADO.NET

Le code que nous avons tudi est propritaire : il dpend de l'espace de noms [System.Data.SqlServerCe] destin au SGBD SQL Server Compact. Nous allons maintenant construire le mme programme avec diffrents connecteurs .NET et voir ce qui change.

7.4.1

Connecteur SQL Server 2005

L'architecture utilise sera la suivante : SGBD Sql Server 2005 3

utilisateur

Application [console] 1

Connecteur [SQL Server 2005] 2

BD 4

L'installation de SQL Server 2005 est dcrite en annexes au paragraphe 1.1, page 435.

Accs aux bases de donnes

232

Nous crons un second projet dans la mme solution que prcdemment puis nous crons la base de donnes SQL server 2005. Le SGBD SQL Server 2005 doit tre lanc avant les oprations qui suivent :

[1] : crer un nouveau projet dans la solution actuelle et en faire le projet courant. [2] : crer une nouvelle connexion [3] : choisir le type de connexion

4 7 8

10 9

[4] : choisir le SGBD SQL Server [5] : rsultat du choix prcdent [6] : utiliser le bouton [Browse] pour indiquer o crer la base SQL Server 2005. La base de donnes est encapsule dans un fichier .mdf. [7] : choisir la racine du nouveau projet et appeler la base [dbarticles.mdf]. [8] : utiliser une authentification windows. [9] : valider la page de renseignements

Accs aux bases de donnes

233

12

13

14 15

11 16

[11] : la base SQL Server [12] : crer une table. Celle-ci sera identique la base SQL Server Compact construite prcdemment. [13] : le champ [id] [14] : le champ [id] est de type Identity. [15,16] : le champ [id] est cl primaire

18

17

[17] : les autres champs de la table [18] : donner le nom [articles] la table au moment de sa sauvegarde (Ctrl+S).

Il nous reste mettre des donnes dans la table :

Nous incluons la base de donnes dans le projet :

Accs aux bases de donnes

234

Les rfrences du projet sont les suivantes :

Le fichier de configuration [App.config] est le suivant :


1. 2. 3. 4. 5. 6. 7. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="connectString1" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=| DataDirectory|\dbarticles.mdf;Integrated Security=True;Connect Timeout=30;User Instance=True;" /> <add name="connectString2" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=| DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;Connect Timeout=30;" /> </connectionStrings> </configuration>

ligne 4 : la chane de connexion la base [dbarticles.mdf] avec une authentification windows ligne 5 : la chane de connexion la base [dbarticles.mdf] avec une authentification SQL Server. [sa,msde] est le couple (login,password) de l'administrateur du serveur SQL Server tel que dfini au paragraphe 1.1, page 435.

Le programme [Program.cs] volue de la faon suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. using System.Data.SqlClient; ... namespace Chap7 { class SqlCommands { static void Main(string[] args) { ... // exploitation du fichier de configuration [App.config] string connectionString = null; try { connectionString = ConfigurationManager.ConnectionStrings["connectString2"].ConnectionString; } catch (Exception e) { ... } ... // lecture-excution des commandes SQL tapes au clavier ... } // excution d'une requte de mise jour static void ExecuteUpdate(string connectionString, string requte) { // on gre les ventuelles exceptions

Accs aux bases de donnes

235

23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56.

try { using (SqlConnection connexion = new SqlConnection(connectionString)) { // ouverture connexion connexion.Open(); // excute sqlCommand avec requte de mise jour SqlCommand sqlCommand = new SqlCommand(requte, connexion); int nbLignes = sqlCommand.ExecuteNonQuery(); // affichage rsultat Console.WriteLine("Il y a eu {0} ligne(s) modifie(s)", nbLignes); } } catch (Exception ex) { .... } // excution d'une requte Select static void ExecuteSelect(string connectionString, string requte) { // on gre les ventuelles exceptions try { using (SqlConnection connexion = new SqlConnection(connectionString)) { // ouverture connexion connexion.Open(); // excute sqlCommand avec requte select SqlCommand sqlCommand = new SqlCommand(requte, connexion); SqlDataReader reader = sqlCommand.ExecuteReader(); // exploitation des rsultats } } catch (Exception ex) { ... } } } } }

...

ligne 1 : l'espace de noms [System.Data.SqlClient] contient les classes permettant de grer une base SQL Server 2005 ligne 24 : la connexion est de type SQLConnection ligne 28 : l'objet encapsulant les ordres SQL est de type SQLCommand ligne 47 : l'objet encapsulant le rsultat d'un ordre SQL Select est de type SQLDataReader

Le code est identique celui utilis avec le SGBD SQL Server Compact au nom des classes prs. Pour l'excuter, on peut utiliser (ligne 11) l'une ou l'autre des deux chanes de connexion dfinies dans [App.config]. 7.4.2 Connecteur MySQL5

L'architecture utilise sera la suivante : SGBD MySQL5 3

utilisateur

Application [console] 1

Connecteur [MySQL5] 2

BD 4

L'installation de MySQL5 est dcrite en annexes au paragraphe 1.2, page 441 et celle du connecteur Ado.Net au paragraphe 1.2.5, page 451. Nous crons un troisime projet dans la mme solution que prcdemment et nous lui ajoutons les rfrences dont il a besoin :

Accs aux bases de donnes

236

3 1

[1] : le nouveau projet [2] : auquel on ajoute des rfrences [3] : la DLL [MySQL.Data] du connecteur Ado.Net de MySql5 ainsi que celle de [System.Configuration] [4].

Nous crons maintenant la base de donnes [dbarticles] et sa table [articles]. Le SGBD MySQL5 doit tre lanc. Par ailleurs, on lance le client [Query Browser] (cf paragraphe 1.2.3, page 445). 1 2 5 4

[1] : dans [Query Browser], cliquer droit dans la zone [Schemata] [2] pour crer [3] un nouveau schma, terme qui dsigne une base de donnes. [4] : la base de donnes s'appellera [dbarticles]. En [5], on la voit. Elle est pour l'instant sans tables. Nous allons excuter le script SQL suivant :
/* choix de la base de donnes courante */ USE dbarticles; /* cration de la table des articles */ CREATE TABLE ARTICLES ( ID INTEGER PRIMARY KEY AUTO_INCREMENT, NOM VARCHAR(20) NOT NULL, PRIX DOUBLE PRECISION NOT NULL, STOCKACTUEL INTEGER NOT NULL, STOCKMINIMUM INTEGER NOT NULL ); /* insertion de donnes dans la table */ INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article1', 100, 10, 1); INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article2', 200, 20, 2); INSERT INTO ARTICLES (NOM, PRIX, STOCKACTUEL, STOCKMINIMUM) VALUES ('article3', 300, 30, 3); /* ajout de contraintes */ ALTER TABLE ARTICLES ADD CONSTRAINT CHK_ID check (ID>0); ALTER TABLE ARTICLES ADD CONSTRAINT CHK_PRIX check (PRIX>0); ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKACTUEL check (STOCKACTUEL>0); ALTER TABLE ARTICLES ADD CONSTRAINT CHK_STOCKMINIMUM check (STOCKMINIMUM>0); ALTER TABLE ARTICLES ADD CONSTRAINT CHK_NOM check (NOM<>''); ALTER TABLE ARTICLES ADD CONSTRAINT UNQ_NOM UNIQUE (NOM);

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21.

ligne 1 : la base [dbarticles] devient la base courante. Les ordres SQL qui suivent s'excuteront sur elle. lignes 4-10 : dfinition de la table [ARTICLES]. On notera le SQL propritaire de MySQL. Les types des colonnes, la gnration automatique de la cl primaire (attribut AUTO_INCREMENT) diffrent de ce qui a t rencontr avec les SGBD SQL Server Compact et Express. lignes 12-14 : insertion de trois lignes lignes 16-21 : ajout de contraintes d'intgrit sur les colonnes.

Ce script est excut dans [MySQL Query Browser] :

Accs aux bases de donnes

237

8 7

dans [MySQL Query Browser] [6], on charge le script [7]. On le voit en [8]. En [9], il est excut. 11 12 13

10

14

en [10], la table [articles] a t cre. On double-clique dessus. Cela fait apparatre la fentre [11] avec la requte [12] dedans prte tre excute par [13]. En [14], le rsultat de l'excution. On a bien les trois lignes attendues. On notera que les valeurs du champ [ID] ont t gnres automatiquement (attribut AUTO_INCREMENT du champ).

Maintenant que la base de donnes est prte, nous pouvons revenir au dveloppement de l'application dans Visual studio.

En [1], le programme [Program.cs] et le fichier de configuration [App.config]. Celui-ci est le suivant :

Accs aux bases de donnes

238

1. 2. 3. 4. 5. 6.

<?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" /> </connectionStrings> </configuration>

Ligne 4, les lments de la chane de connexion sont les suivants : Server : nom de la machine sur laquelle se trouve le SGBD MySQL, ici localhost, c.a.d. la machine sur laquelle va tre excute le programme. Database : le nom de la base de donnes gre, ici dbarticles Uid : le login de l'utilisateur, ici root Pwd : son mot de passe, ici root. Ces deux informations dsignent l'administrateur cr au paragraphe 1.2, page 441. Le programme [Program.cs] est identique celui des versions prcdentes aux dtails prs suivants :
espace classe classe classe de noms Connection Command DataReader

MySql.Data.MySqlClient MySqlConnection MySqlCommand MySqlDataReader

Le programme utilise la chane de connexion nomme dbArticlesMySql5 dans le fichier [App.config]. L'excution donne les rsultats suivants :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. Chane de connexion la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;] Requte SQL (rien pour arrter) : select * from articles -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------1 article1 100 10 1 2 article2 200 20 2 3 article3 300 30 3

7.4.3

Connecteur ODBC

L'architecture utilise sera la suivante : SGBD MySQL5 SGBD SQL Server BD BD

utilisateur

Application [console]

Connecteur ODBC [MySQL5] Connecteur ODBC [SQL Server]

L'intrt des connecteurs ODBC est qu'ils prsentent une interface standard aux applications qui les utilisent. Ainsi la nouvelle application va-t-elle, avec un unique code, pouvoir dialoguer avec tout SGBD ayant un connecteur ODBC, c.a.d. la plupart des SGBD. Les performances des connecteurs ODBC sont moins bonnes que celles des connecteurs "propritaires" qui savent exploiter toutes les caractristiques d'un SGBD particulier. En contre-partie, on obtient une grande souplesse de l'application : on peut changer de SGBD sans changer le code. Nous tudions un exemple o l'application exploite une base MySQL5 ou une base SQL server Express selon la chane de connexion qu'on lui donne. Dans ce qui suit, nous supposons que : les SGBD SQL Server Express et MySQL5 ont t lancs que le pilote ODBC de MySQL5 est prsent sur la machine (cf paragraphe 1.2.6, page 451). Celui de SQL Server 2005 est prsent par dfaut. les bases de donnes utilises sont celles du paragraphe 7.4.2, page 236 pour la base MySQL5, celle du paragraphe 7.4.1, page 232 pour la base SQL Server Express. Le nouveau projet Visual studio est le suivant :

Accs aux bases de donnes

239

Ci-dessus, la base SQL Server [dbarticles.mdf] cre au paragraphe 7.4.1, page 232 a t recopie dans le dossier du projet. Le fichier de configuration [App.config] est le suivant :
1. 2. 3. 4. 5. 6. 7. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;" /> <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" /> </connectionStrings> </configuration>

ligne 4 : la chane de connexion de la source ODBC MySQL5. C'est une chane dj tudie dans laquelle on trouve un nouveau paramtre Driver qui dfinit le pilote ODBC utiliser. ligne 5 : la chane de connexion de la source ODBC SQL Server Express. C'est la chane dj utilise dans un exemple prcdent laquelle le paramtre Driver a t ajout.

Le programme [Program.cs] est identique celui des versions prcdentes aux dtails prs suivants :
espace classe classe classe de noms Connection Command DataReader

System.Data.Odbc OdbcConnection OdbcCommand OdbcDataReader

Le programme utilise l'une des deux chanes de connexion dfinies dans le fichier [App.config]. L'excution donne les rsultats suivants : Avec la chane de connexion [dbArticlesOdbcSqlServer2005] :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. Chane de connexion la base : [Driver={SQL Native Client};Server=.\SQLExpress;AttachDbFilename=| DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;] Requte SQL (rien pour arrter) : select * from articles -----------------------------------id,nom,prix,stockactuel,stockminimum -----------------------------------1 2 3 4 5 6 vlo 500,0000 10 5 pompe 10,0000 10 2 arc 610,0000 4 1 flches - lot de 6 100,0000 12 20 combinaison de plonge 300,0000 8 2 Bouteilles d'oxygne 120,0000 10 5

Avec la chane de connexion [dbArticlesOdbcMySql5] :


1. 2. 3. 4. 5. 6. Chane de connexion la base : [Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;] Requte SQL (rien pour arrter) : select * from articles -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM

Accs aux bases de donnes

240

7. 8. 9. 10. 11.

-----------------------------------1 article1 100 10 1 2 article2 200 20 2 3 article3 300 30 3

7.4.4

Connecteur OLE DB

L'architecture utilise sera la suivante : SGBD ACCESS SGBD SQL Server BD BD

utilisateur

Application [console]

Connecteur OLE DB [ACCESS] Connecteur OLE DB [SQL Server]

Comme les connecteurs ODBC, les connecteurs OLE DB (Object Linking and Embedding DataBase) prsentent une interface standard aux applications qui les utilisent. Les pilotes ODBC permettent l'accs des bases de donnes. Les sources de donnes pour les pilotes OLE DB sont plus varies : bases de donnes, messageries, annuaires, ... Toute source de donnes peut faire l'objet d'un pilote Ole DB si un diteur le dcide. On a ainsi un accs standard une grande varit de donnes. Nous tudions un exemple o l'application exploite une base ACCESS ou une base SQL server Express selon la chane de connexion qu'on lui donne. Dans ce qui suit, nous supposons que le SGBD SQL Server Express a t lanc et que la base de donnes utilise est celle de l'exemple prcdent. Le nouveau projet Visual studio est le suivant : 1

en [1] : l'espace de noms ncessaire aux connecteurs OLE DB est [System.Data.OleDb] prsent dans la rfrence [System.Data] ci-dessus. La base SQL Server [dbarticles.mdf] a t recopie partir du projet prcdent. La base [dbarticles.mdb] a t cre avec Access. en [2] : comme la base SQL Server, la base ACCESS a la proprit [Copy to Output Directory=Copy Always] afin qu'elle soit automatiquement recopie dans le dossier d'excution du projet.

La base de donnes ACCESS [dbarticles.mdb] est la suivante :

1 2

Accs aux bases de donnes

241

En [1], la structure de la table [articles] et en [2] son contenu. Le fichier de configuration [App.config] est le suivant :
1. 2. 3. 4. 5. 6. 7. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbArticlesOleDbAccess" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;"/> <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLEXPRESS;AttachDbFilename=| DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" /> </connectionStrings> </configuration>

ligne 4 : la chane de connexion de la source OLE DB ACCESS. On y trouve le paramtre Provider qui dfinit le pilote OLE DB utiliser ainsi que le chemin de la base de donnes ligne 5 : la chane de connexion de la source OLE DB Server Express.

Le programme [Program.cs] est identique celui des versions prcdentes aux dtails prs suivants :
espace classe classe classe de noms Connection Command DataReader

System.Data.OleDb OleDbConnection OleDbCommand OleDbDataReader

Le programme utilise l'une des deux chanes de connexion dfinies dans le fichier [App.config]. L'excution donne les rsultats suivants avec la chane de connexion [dbArticlesOleDbAccess] :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. Chane de connexion la base : [Provider=Microsoft.Jet.OLEDB.4.0;Data Source=| DataDirectory|\dbarticles.mdb;] Requte SQL (rien pour arrter) : select * from articles -----------------------------------id,nom,prix,stockactuel,stockminimum -----------------------------------1 2 3 4 5 6 7 8 9 vlo 1202 5 2 arc 5000 10 2 cano 1502 12 6 fusil 3000 10 2 skis nautiques 1800 5 2 essai3 3 3 3 cachalot 200000 1 0 lopard 500000 1 1 panthre 800000 1 1

7.4.5

Connecteur gnrique

L'architecture utilise sera la suivante : SGBD MySQL5 SGBD SQL Server BD BD

utilisateur

Application .NET

Connecteur gnrique

Connecteur [MySQL5] Connecteur [SQL Server]

Comme les connecteurs ODBC et OLE DB, le connecteur gnrique prsente une interface standard aux applications qui l'utilise mais amliore les performances sans sacrifier la souplesse. En effet, le connecteur gnrique s'appuie sur les connecteurs propritaires des SGBD. L'application utilise des classes du connecteur gnrique. Ces classes servent d'intermdiaires entre l'application et le connecteur propritaire. Ci-dessus, lorsque l'application demande par exemple une connexion au connecteur gnrique, celui-ci lui rend une instance IDbConnection, l'interface des connexions dcrite page 225, implmente par une classe MySQLConnection ou

Accs aux bases de donnes

242

SQLConnection selon la nature de la demande qui lui a t faite. On dit que le connecteur gnrique a des classes de type factory : on utilise une classe factory pour lui demander de crer des objets et en donner des rfrences (pointeurs). D'o son nom (factory=usine, usine de production d'objets). Il n'existe pas de connecteur gnrique pour tous les SGBD (avril 2008). Pour connatre ceux installs sur une machine, on pourra utiliser le programme suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. using System; using System.Data; using System.Data.Common; namespace Chap7 { class Providers { public static void Main() { DataTable dt = DbProviderFactories.GetFactoryClasses(); foreach (DataColumn col in dt.Columns) { Console.Write("{0}|", col.ColumnName); } Console.WriteLine("\n".PadRight(40, '-')); foreach (DataRow row in dt.Rows) { foreach (object item in row.ItemArray) { Console.Write("{0}|", item); } Console.WriteLine("\n".PadRight(40, '-')); } } } }

ligne 8 : la mthode statique [DbProviderFactories.GetFactoryClasses()] rend la liste des connecteurs gnriques installs, sous la forme d'une table de base de donnes place en mmoire (DataTable). lignes 9-11 : affichent les noms des colonnes de la table dt : dt.Columns est la liste des colonnes de la table. Une colonne C est de type DataColumn [DataColumn].ColumnName est le nom de la colonne lignes 13-18 : affichent les lignes de la table dt : dt.Rows est la liste des lignes de la table. Une ligne L est de type DataRow [DataRow].ItemArray est un tableau d'objets ou chaque objet reprsente une colonne de la ligne

Le rsultat de l'excution sur ma machine est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. Name|Description|InvariantName|AssemblyQualifiedName| --------------------------------------Odbc Data Provider|.Net Framework Data Provider for Odbc|System.Data.Odbc| System.Data.Odbc.OdbcFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089| --------------------------------------OleDb Data Provider|.Net Framework Data Provider for OleDb|System.Data.OleDb| System.Data.OleDb.OleDbFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089| --------------------------------------OracleClient Data Provider|.Net Framework Data Provider for Oracle|System.Data.OracleClient| System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089| --------------------------------------SqlClient Data Provider|.Net Framework Data Provider for SqlServer|System.Data.SqlClient| System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089| --------------------------------------Microsoft SQL Server Compact Data Provider|.NET Framework Data Provider for Microsoft SQL Server Compact|System.Data.SqlServerCe.3.5|System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=3.5.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91| --------------------------------------MySQL Data Provider|.Net Framework Data Provider for MySQL|MySql.Data.MySqlClient| MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=5.2.1.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d|

ligne 1 : la table a quatre colonnes. Les trois premires sont les plus utiles pour nous ici.

L'affichage suivant montre que l'on dispose des connecteurs gnriques suivants : Nom Identifiant

Accs aux bases de donnes

243

Odbc Data Provider OleDb Data Provider OracleClient Data Provider SqlClient Data Provider Microsoft SQL Server Compact Data Provider MySQL Data Provider

System.Data.Odbc System.Data.OleDb System.Data.OracleClient System.Data.SqlClient System.Data.SqlServerCe.3.5 MySql.Data.MySqlClient

Un connecteur gnrique est accessible dans un programme C# via son identifiant. Nous tudions un exemple o l'application exploite les diverses bases de donnes que nous avons construites jusqu' maintenant. L'application recevra deux paramtres : un premier paramtre prcise le type de SGBD utilis afin que la bonne bibliothque de classes soit utilise le second paramtre prcise la base de donnes gre, via une chane de connexion. Le nouveau projet Visual studio est le suivant : 1

en [1] : l'espace de noms ncessaire aux connecteurs gnriques est [System.Data.common] prsent dans la rfrence [System.Data].

Le fichier de configuration [App.config] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbArticlesSqlServerCe" connectionString="Data Source=| DataDirectory|\dbarticles.sdf;Password=dbarticles;" /> <add name="dbArticlesSqlServer" connectionString="Data Source=.\SQLEXPRESS;AttachDbFilename=| DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" /> <add name="dbArticlesMySql5" connectionString="Server=localhost;Database=dbarticles;Uid=root;Pwd=root;" /> <add name="dbArticlesOdbcMySql5" connectionString="Driver={MySQL ODBC 3.51 Driver};Server=localhost;Database=dbarticles; User=root;Password=root;Option=3;" /> <add name="dbArticlesOleDbSqlServer2005" connectionString="Provider=SQLNCLI;Server=.\SQLExpress;AttachDbFilename=| DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" /> <add name="dbArticlesOdbcSqlServer2005" connectionString="Driver={SQL Native Client};Server=.\ SQLExpress;AttachDbFilename=|DataDirectory|\dbarticles.mdf;Uid=sa;Pwd=msde;" /> <add name="dbArticlesOleDbAccess" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\dbarticles.mdb;Persist Security Info=True"/> </connectionStrings> <appSettings> <add key="factorySqlServerCe" value="System.Data.SqlServerCe.3.5"/> <add key="factoryMySql" value="MySql.Data.MySqlClient"/> <add key="factorySqlServer" value="System.Data.SqlClient"/> <add key="factoryOdbc" value="System.Data.Odbc"/> <add key="factoryOleDb" value="System.Data.OleDb"/> </appSettings> </configuration>

lignes 3-11 : les chanes de connexion des diverses bases de donnes exploites. lignes 13-17 : les noms des connecteurs gnriques utiliser

Accs aux bases de donnes

244

Le programme [Program.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. ... using System.Data.Common; namespace Chap7 { class SqlCommands { static void Main(string[] args) { // application console - excute des requtes SQL tapes au clavier // sur une base de donnes dont la chane de connexion est obtenue dans un fichier de configuration ainsi que le nom du connecteur du SGBD associ

10. 11. // vrification paramtres 12. if (args.Length != 2) { 13. Console.WriteLine("Syntaxe : pg factory connectionString"); 14. return; 15. } 16. 17. // exploitation du fichier de configuration 18. string factory = null; 19. string connectionString = null; 20. DbProviderFactory connecteur = null; 21. try { 22. // factory 23. factory = ConfigurationManager.AppSettings[args[0]]; 24. // chane de connexion 25. connectionString = ConfigurationManager.ConnectionStrings[args[1]].ConnectionString; 26. // on rcupre un connecteur gnrique pour le SGBD 27. connecteur = DbProviderFactories.GetFactory(factory); 28. } catch (Exception e) { 29. Console.WriteLine("Erreur de configuration : {0}", e.Message); 30. return; 31. } 32. 33. // affichages 34. Console.WriteLine("Provider factory : [{0}]\n", factory); 35. Console.WriteLine("Chane de connexion la base : [{0}]\n", connectionString); 36. 37. ... 38. // excution de la requte 39. if (champs[0] == "select") { 40. ExecuteSelect(connecteur,connectionString, requte); 41. } else 42. ExecuteUpdate(connecteur, connectionString, requte); 43. } 44. } 45. 46. // excution d'une requte de mise jour 47. static void ExecuteUpdate(DbProviderFactory connecteur, string connectionString, string requte) { 48. // on gre les ventuelles exceptions 49. try { 50. using (DbConnection connexion = connecteur.CreateConnection()) { 51. // configuration connexion 52. connexion.ConnectionString = connectionString; 53. // ouverture connexion 54. connexion.Open(); 55. // configuration Command 56. DbCommand sqlCommand = connecteur.CreateCommand(); 57. sqlCommand.CommandText = requte; 58. sqlCommand.Connection = connexion; 59. // excution requte 60. int nbLignes = sqlCommand.ExecuteNonQuery(); 61. // affichage rsultat 62. Console.WriteLine("Il y a eu {0} ligne(s) modifie(s)", nbLignes); 63. } 64. } catch (Exception ex) { 65. // msg d'erreur 66. Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); 67. } 68. } 69. 70. // excution d'une requte Select 71. static void ExecuteSelect(DbProviderFactory connecteur, string connectionString, string requte) { 72. // on gre les ventuelles exceptions 73. try {

Accs aux bases de donnes

245

74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. ... 87. 88. 89. 90. 91. 92. } 93. } 94. }

using (DbConnection connexion = connecteur.CreateConnection()) { // configuration connexion connexion.ConnectionString = connectionString; // ouverture connexion connexion.Open(); // configuration Command DbCommand sqlCommand = connecteur.CreateCommand(); sqlCommand.CommandText = requte; sqlCommand.Connection = connexion; // excution requte DbDataReader reader = sqlCommand.ExecuteReader(); // affichage des rsultats } } catch (Exception ex) { // msg d'erreur Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); }

lignes 12-14 : l'application reoit deux paramtres : le nom du connecteur gnrique ainsi que la chane de connexion la base de donnes sous la forme de cls du fichier [App.config]. lignes 23, 25 : on rcupre dans [App.config], le nom du connecteur gnrique ainsi que la chane de connexion ligne 27 : le connecteur gnrique est instanci. A partir de ce moment, il est associ un SGBD particulier. lignes 39-43 : l'excution de l'ordre SQL saisi au clavier est dlgue deux mthodes auxquelles on passe : la requte excuter la chane de connexion qui identifie la base sur laquelle la requte sera excute le connecteur gnrique qui identifie les classes utiliser pour dialoguer avec le SGBD grant la base. lignes 50-54 : une connexion est obtenue avec la mthode CreateConnection (ligne 50) du connecteur gnrique puis configure avec la chane de connexion de la base grer (ligne 52). Elle est ensuite ouverte (ligne 54). lignes 56-58 : l'objet Command ncessaire l'excution de l'ordre SQL est cr avec la mthode CreateCommand du connecteur gnrique. Il est ensuite configur avec le texte de l'ordre SQL excuter (ligne 57) et la connexion sur laquelle excuter celui-ci (ligne 58). ligne 60 : l'ordre SQL de mise jour est excut lignes 74-87 : on trouve un code analogue. La nouveaut se trouve ligne 84. L'objet Reader obtenu par l'excution de l'ordre Select est de type DbDataReader qui s'utilise comme les objets OleDbDataReader, OdbcDataReader, ... que nous avons dj rencontrs.

Voici quelques exemples d'excution. Avec la base MySQL5 : 1 4 3 2

On ouvre la page de proprits du projet [1] et on slectionne l'onglet [Debug] [2]. En [3], la cl du connecteur de la ligne 14 de [App.config]. En [4], la cl de la chane de connexion de la ligne 6 de [App.config]. Les rsultats de l'excution sont les suivants :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. Provider factory : [MySql.Data.MySqlClient] Chane de connexion la base : [Server=localhost;Database=dbarticles;Uid=root;Pwd=root;] Requte SQL (rien pour arrter) : select * from articles -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------1 article1 100 10 1

Accs aux bases de donnes

246

11. 2 article2 200 20 2 12. 3 article3 300 30 3

Avec la base SQL Server Compact :

2 1

En [1], la cl du connecteur de la ligne 13 de [App.config]. En [2], la cl de la chane de connexion de la ligne 4 de [App.config]. Les rsultats de l'excution sont les suivants :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. Provider factory : [System.Data.SqlServerCe.3.5] Chane de connexion la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;] Requte SQL (rien pour arrter) : select * from articles -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------1 2 3 4 5 6 vlo 500 10 5 pompe 10 10 2 arc 600 4 1 flches - lot de 6 100 12 20 combinaison de plonge 300 8 2 bouteilles d'oxygne 120 10 5

Le lecteur est invit tester les autres bases de donnes. 7.4.6 Quel connecteur choisir ?

Revenons l'architecture d'une application avec bases de donnes :

utilisateur

Couche d'accs aux donnes [dao] 1

Connecteur [ADO.NET] 2

SGBD 3

BD 4

Nous avons vu divers types de connecteurs ADO.NET : les connecteurs propritaires sont les plus performants mais rendent la couche [dao] dpendante de classes propritaires. Changer le SGBD implique de changer la couche [dao]. les connecteurs ODBC ou OLE DB permettent de travailler avec de multiples bases de donnes sans changer la couche [dao]. Ils sont moins performants que les connecteurs propritaires. le connecteur gnrique s'appuie sur les connecteurs propritaires tout en prsentant une interface standard la couche [dao]. Il semble donc que le connecteur gnrique soit le connecteur idal. Dans la pratique, le connecteur gnrique n'arrive cependant pas cacher toutes les particularits d'un SGBD derrire une interface standard. Nous allons voir dans le paragraphe suivant, la notion de requte paramtre. Avec SQL Server, une requte paramtre la forme suivante :
insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

Avec MySQL5, la mme requte s'crirait :


insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Accs aux bases de donnes

247

Il y a donc une diffrence de syntaxe. La proprit de l'interface IDbCommand dcrite page 225, lie aux paramtres est la suivante :
Parameters

la liste des paramtres d'un ordre SQL paramtr. L'ordre update articles set prix=prix*1.1 where id=@id a le paramtre @id.

La proprit Parameters est de type IDataParameterCollection, une interface. Elle reprsente l'ensemble des paramtres de l'ordre SQL CommandText. La proprit Parameters a une mthode Add pour ajouter des paramtres de type IDataParameter, de nouveau une interface. Celle-ci a les proprits suivantes :

ParameterName : nom du paramtre DbType : le type SQL du paramtre Value : la valeur affecte au paramtre ...

Le type IDataParameter convient bien aux paramtres de l'ordre SQL


insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

car on y trouve des paramtres nomms. La proprit ParameterName peut tre utilise. Le type IDataParameter ne convient pas l'ordre SQL
insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

car les paramtres ne sont pas nomms. C'et l'ordre d'ajout des paramtres dans la collection [IDbCommand.Parameters] qui est alors pris en compte. Dans cet exemple, il faudra insrer les 4 paramtres dans l'ordre nom, prix, stockactuel, stockminimum. Dans la requte avec paramtres nomms, l'ordre d'ajout des paramtres n'a pas d'importance. Au final, le dveloppeur ne peut faire totalement abstraction du SGBD qu'il utilise lorsqu'il initialise les paramtres d'une requte paramtre. On a l une des limites actuelles du connecteur gnrique. Il existe des frameworks qui s'affranchissent de ces limites et qui apportent par ailleurs de nouvelles fonctionnalits la couche [dao] : SGBD MySQL5 SGBD SQL Server BD BD

utilisateur

couche [dao]

Framework

Connecteur [MySQL5] Connecteur [SQL Server]

Un framework est un ensemble de bibliothques de classes visant faciliter une certaine faon d'architecturer l'application. Il en existe plusieurs qui permettent l'criture de couches [dao] la fois performantes et insensibles au changement de SGBD :

Spring.Net [http://www.springframework.net/] dj prsent dans ce document offre l'quivalent du connecteur gnrique tudi, sans ses limitations, ainsi que des facilits diverses qui simplifient l'accs aux donnes. Il existe une version Java. iBatis.Net [http://ibatis.apache.org] est plus ancien et plus riche que Spring.Net. Il existe une version Java. NHibernate [http://www.hibernate.org/] est un portage de la version Java Hibernate trs connue dans le monde Java. NHibernate permet la couche [dao] d'changer avec le SGBD sans mettre d'ordres SQL. La couche [dao] travaille avec des objets Hibernate. Un langage de requtes HBL (Hibernate Query language) permet de requter les objets grs par Hibernate. Ces sont ces derniers qui mettent les ordres SQL. Hibernate sait s'adapter aux SQL propritaires des SGBD. LINQ (Language INtegrated Query), intgre la version 3.5 .NET et disponible dans C# 2008. LINQ marche sur les pas de NHibernate, mais pour l'instant (mai 2008) seul le SGBD SQL Server est support. Ceci devrait voluer avec le temps. LINQ va plus loin que NHibernate : son langage de requtes permet d'interroger de faon standard trois types diffrents de sources de donnes : des collections d'objets (LINQ to Objects) un fichier Xml (LINQ to Xml) une base de donnes (LINQ to SQL)

Ces frameworks ne seront pas abords dans ce document. Il est cependant vivement conseill de les utiliser dans les applications professionnelles.

Accs aux bases de donnes

248

7.5

Requtes paramtres

Nous avons voqu dans le paragraphe prcdent les requtes paramtres. Nous les prsentons ici avec un exemple pour le SGBD SQL Server Compact. Le projet est le suivant 1 3

en [1], le projet. Seuls [App.config], [Article.cs] et [Parametres.cs] sont utiliss. On notera galement la base SQL Server Ce [dbarticles.sdf]. en [2], le projet est configur pour excuter [Parametres.cs] en [3], les rfrences du projet

Le fichier de configuration [App.config] dfinit la chane de connexion la base de donnes :


1. 2. 3. 4. 5. 6. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbArticlesSqlServerCe" connectionString="Data Source=| DataDirectory|\dbarticles.sdf;Password=dbarticles;" /> </connectionStrings> </configuration>

Le fichier [Article.cs] dfinit une classe [Article]. Un objet Article sera utilis pour encapsuler les informations d'une ligne de la table ARTICLES de la base de donnes [dbarticles.sdf] :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. namespace Chap7 { class Article { // proprits public int Id { get; set; } public string Nom { get; set; } public decimal Prix { get; set; } public int StockActuel { get; set; } public int StockMinimum { get; set; } // constructeurs public Article() { } public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) { Id = id; Nom = nom; Prix = prix; StockActuel = stockActuel; StockMinimum = stockMinimum; } } }

L'application [Parametres.cs] met en oeuvre les requtes paramtres :


1. 2. 3. 4. 5. 6. using using using using using System; System.Data.SqlServerCe; System.Text; System.Data; System.Configuration;

Accs aux bases de donnes

249

7. namespace Chap7 { 8. class Parametres { 9. static void Main(string[] args) { 10. 11. // exploitation du fichier de configuration 12. string connectionString = null; 13. try { 14. // chane de connexion 15. connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString; 16. } catch (Exception e) { 17. Console.WriteLine("Erreur de configuration : {0}", e.Message); 18. return; 19. } 20. 21. // affichages 22. Console.WriteLine("Chane de connexion la base : [{0}]\n", connectionString); 23. 24. // cration d'un tableau d'articles 25. Article[] articles = new Article[5]; 26. for (int i = 1; i <= articles.Length; i++) { 27. articles[i-1] = new Article(0, "article" + i, i * 100, i * 10, i); 28. } 29. 30. // on gre les ventuelles exceptions 31. try { 32. 33. // on supprime les articles existants de la base 34. ExecuteUpdate(connectionString, "delete from articles"); 35. 36. // on affiche les articles de la table 37. ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles"); 38. 39. // on insre le tableau des articles dans la base 40. InsertArticles(connectionString, articles); 41. 42. // on affiche les articles de la table 43. ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles"); 44. } catch (Exception ex) { 45. // msg d'erreur 46. Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); 47. } 48. } 49. 50. // insertion d'un tableau d'articles 51. static void InsertArticles(string connectionString, Article[] articles) { 52. using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) { 53. // ouverture connexion 54. connexion.Open(); 55. // configuration commande 56. string requte = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"; 57. SqlCeCommand sqlCommand = new SqlCeCommand(requte, connexion); 58. sqlCommand.Parameters.Add("@nom",SqlDbType.NVarChar,30); 59. sqlCommand.Parameters.Add("@prix", SqlDbType.Money); 60. sqlCommand.Parameters.Add("@sa", SqlDbType.Int); 61. sqlCommand.Parameters.Add("@sm", SqlDbType.Int); 62. // compilation de la commande 63. sqlCommand.Prepare(); 64. // insertion des lignes 65. for (int i = 0; i < articles.Length; i++) { 66. // initialisation paramtres 67. sqlCommand.Parameters["@nom"].Value = articles[i].Nom; 68. sqlCommand.Parameters["@prix"].Value = articles[i].Prix; 69. sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel; 70. sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum; 71. // excution requte 72. sqlCommand.ExecuteNonQuery(); 73. } 74. } 75. } 76. 77. // excution d'une requte de mise jour 78. static void ExecuteUpdate(string connectionString, string requte) { 79. ... 80. }

Accs aux bases de donnes

250

81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91.

// excution d'une requte Select static void ExecuteSelect(string connectionString, string requte) { ... } // affichage reader static void AfficheReader(IDataReader reader) {

... } }

La nouveaut par rapport ce qui a t vu prcdemment est la procdure [InsertArticles] des lignes 51-75 :

ligne 51 : la procdure reoit deux paramtres : la chane de connexion connectionString qui va permettre la procdure de se connecter la base un tableau d'objets Article qu'il faut ajouter la table Articles de la base de donnes ligne 56 : la requte d'insertion d'un objet [Article]. Elle a quatre paramtres : @nom : le nom de l'article @prix : son prix @sa : son stock actuel @sm : son stock minimum La syntaxe de cette requte paramtre est propritaire SQL Server Compact. Nous avons vu dans le paragraphe prcdent qu'avec MySQL5, la syntaxe serait la suivante :
insert into articles(nom,prix,stockactuel,stockminimum) values(?,?,?,?)

Avec SQL Server Compact, chaque paramtre doit tre prcd du caractre @. Le nom des paramtres est libre. lignes 58-61 : on dfinit les caractristiques de chacun des 4 paramtres et on les ajoute, un par un, la liste des paramtres de l'objet SqlCeCommand qui encapsule l'ordre SQL qui va tre excut. On utilise ici la mthode [SqlCeCommand].Parameters.Add qui possde six signatures. Nous utilisons les deux suivantes : Add(string parameterName, SQLDbType type) ajoute et configure le paramtre nomm parameterName. Ce nom doit tre l'un de ceux de la requte paramtre configure : (@nom, ...). type dsigne le type SQL de la colonne concerne par le paramtre. On dispose de nombreux types dont les suivants : type SQL
BigInt DateTime Decimal Float Int Money NChar NVarChar Real

type C# Int64 DateTime Decimal Double Int32 Decimal String String Single

commentaire

chane de longueur fixe chane de longueur variable

Add(string parameterName, SQLDbType type, int size) le troisime paramtre size fixe la taille de la colonne. Cette information n'est utile que pour certains types SQL, le type NVarChar par exemple.

ligne 63 : on compile la requte paramtre. On dit aussi qu'on la prpare, d'o le nom de la mthode. Cette opration n'est pas indispensable. Elle est l pour amliorer les performances. Lorsqu'un SGBD excute un ordre SQL, il fait un certain travail d'optimisation avant de l'excuter. Une requte paramtre est destine tre excute plusieurs fois avec des paramtres diffrents. Le texte de la requte lui ne change pas. Le travail d'optimisation peut alors n'tre fait qu'une fois. Certains SGBD ont la possibilit de "prparer" ou "compiler" des requtes paramtres. Un plan d'excution est alors

Accs aux bases de donnes

251

dfini pour cette requte. C'est la phase d'optimisation dont on a parl. Une fois compile, la requte est excute de faon rpte avec chaque fois de nouveaux paramtres effectifs mais le mme plan d'excution. La compilation n'est pas l'unique avantage des requtes paramtres. Reprenons la requte tudie :
insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)

On pourrait vouloir construire le texte de la requte par programme :


string requte="insert into articles(nom,prix,stockactuel,stockminimum) values('"+nom+"',"+prix+","+sa+","+sm+")";

Ci-dessus si (nom,prix,sa,sm) vaut ("article1",100,10,1), la requte prcdente devient :


string requte="insert into articles(nom,prix,stockactuel,stockminimum) values('article1',100,10,1)";

Maintenant si (nom,prix,sa,sm) vaut ("l'article1",100,10,1), la requte prcdente devient :


string requte="insert into articles(nom,prix,stockactuel,stockminimum) values('l'article1',100,10,1)";

et devient syntaxiquement incorrecte cause de l'apostrophe du nom l'article1. Si nom provient d'une saisie de l'utilisateur, cela veut dire que nous sommes amens vrifier si la saisie n'a pas d'apostrophes et si elle en a, les neutraliser. Cette neutralisation est dpendante du SGBD. L'intrt de la requte prpare est qu'elle fait elle-mme ce travail. Cette facilit justifie elle seule l'utilisation d'une requte prpare.

lignes 65-73 : les articles du tableau sont insrs un un lignes 67-70 : chacun des quatre paramtres de la requte reoit sa valeur via sa proprit Value. ligne 72 : la requte d'insertion maintenant complte est excute de la faon habituelle.

Voici un exemple d'excution :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. Chane de connexion la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;] Il y a eu 5 ligne(s) modifie(s) -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM ----------------------------------------------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------117 118 119 120 121 article1 article2 article3 article4 article5 100 200 300 400 500 10 20 30 40 50 1 2 3 4 5

ligne 3 : message aprs la suppression de toutes les lignes de la table lignes 5-7 : montrent que la table est vide lignes 10-18 : montrent la table aprs l'insertion des 5 articles

7.6
7.6.1

Transactions
Gnralits

Une transaction est une suite d'ordres SQL excute de faon "atomique" : soit toutes les oprations russissent soit l'une d'elles choue et alors toutes celles qui ont prcd sont annules

Accs aux bases de donnes

252

Au final, les oprations d'une transaction ont soit toutes t appliques avec succs, soit aucune n'a t applique. Lorsque l'utilisateur a lui-mme la matrise de la transaction, il valide une transaction par un ordre COMMIT ou l'annule par un ordre ROLLBACK. Dans nos exemples prcdents, nous n'avons pas utilis de transaction. Et pourtant il y en avait, car dans un SGBD un ordre SQL s'excute toujours au sein d'une transaction. Si le client .NET ne dmarre pas lui-mme une transaction explicite, le SGBD utilise une transaction implicite. Il y a alors deux cas courants : 1. 2. chaque ordre SQL individuel fait l'objet d'une transaction, initie par le SGBD avant l'ordre et ferme ensuite. On dit qu'on est en mode autocommit. Tout se passe donc comme si le client .NET faisait des transactions pour chaque ordre SQL. le SGBD n'est pas en mode autocommit et commence une transaction implicite au 1er ordre SQL que le client .NET met en dehors d'une transaction et il laisse le client la fermer. Tous les ordres SQL mis par le client .NET font alors partie de la transaction implicite. Celle-ci peut se terminer sur diffrents vnements : le client ferme la connexion, commence une nouvelle transaction, ... mais on est alors dans une situation dpendante du SGBD. Cest un mode viter.

Le mode par dfaut est gnralement fix par configuration du SGBD. Certains SGBD sont par dfaut en mode autocommit, d'autres pas. SQLServer Compact est par dfaut en mode autocommit. Les ordres SQL des diffrents utilisateurs s'excutent en mme temps dans des transactions qui travaillent en parallle. Les oprations faites par une transaction peuvent affecter celles faites par une autre transaction. On distingue quatre niveaux d'tanchit entre les transactions des diffrents utilisateurs :

Uncommitted Read Committed Read Repeatable Read Serializable

Uncommitted Read Ce mode d'isolation est galement appel "Dirty Read". Voici un exemple de ce qui se peut se passer dans ce mode : 1. un utilisateur U1 commence une transaction sur une table T 2. un utilisateur U2 commence une transaction sur cette mme table T 3. l'utilisateur U1 modifie des lignes de la table T mais ne les valide pas encore 4. l'utilisateur U2 "voit" ces modifications et prend des dcisions partir de ce qu'il voit 5. l'utilisateur annule sa transaction par un ROLLBACK On voit qu'en 4, l'utilisateur U2 a pris une dcision partir de donnes qui s'avreront fausses ultrieurement. Committed Read Ce mode d'isolation vite l'cueil prcdent. Dans ce mode, l'utilisateur U2 l'tape 4 ne "verra" pas les modifications apportes par l'utilisateur U1 la table T. Il ne les verra qu'aprs que U1 ait fait un COMMIT de sa transaction. Dans ce mode, galement appel "Unrepeatable Read", on peut nanmoins rencontrer les situations suivantes : 1. 2. 3. 4. 5. un utilisateur U1 commence une transaction sur une table T un utilisateur U2 commence une transaction sur cette mme table T l'utilisateur U2 fait un SELECT pour obtenir la moyenne d'une colonne C des lignes de T vrifiant une certaine condition l'utilisateur U1 modifie (UPDATE) certaines valeurs de la colonne C de T et les valide (COMMIT) l'utilisateur U2 refait le mme SELECT qu'en 3. Il dcouvrira que la moyenne de la colonne C a chang cause des modifications faites par U1.

Maintenant l'utilisateur U2 ne voit que les modifications "valides" par U1. Mais alors qu'il reste dans la mme transaction, deux oprations identiques 3 et 5 donnent des rsultats diffrents. Le terme "Unrepeatable Read" dsigne cette situation. C'est une situation ennuyeuse pour quelqu'un qui dsire avoir une image stable de la table T. Repeatable Read Dans ce mode d'isolation, un utilisateur est assur d'avoir les mmes rsultats pour ses lectures de la base tant qu'il reste dans la mme transaction. Il travaille sur une photo sur laquelle ne sont jamais rpercutes les modifications apportes par les autres transactions, mmes valides. Il ne verra celles-ci que lorsque lui-mme terminera sa transaction par un COMMIT ou ROLLBACK.

Accs aux bases de donnes

253

Ce mode d'isolation n'est cependant pas encore parfait. Aprs l'opration 3 ci-dessus, les lignes consultes par l'utilisateur U2 sont verrouilles. Lors de l'opration 4, l'utilisateur U1 ne pourra pas modifier (UPDATE) les valeurs de la colonne C de ces lignes. Il peut cependant rajouter des lignes (INSERT). Si certaines des lignes ajoutes vrifient la condition teste en 3, l'opration 5 donnera une moyenne diffrente de celle trouve en 3 cause des lignes rajoutes. On appelle parfois ces lignes des lignes fantmes. Pour rsoudre ce nouveau problme, il faut passer en isolation "Serializable". Serializable Dans ce mode d'isolation, les transactions sont compltement tanches les unes des autres. Il assure que le rsultat de deux transactions menes simultanment donneront le mme rsultat que si elles taient faites l'une aprs l'autre. Pour arriver ce rsultat, lors de l'opration 4 o l'utilisateur U1 veut ajouter des lignes qui changeraient le rsultat du SELECT de l'utilisateur U1, il en sera empch. Un message d'erreur lui indiquera que l'insertion n'est pas possible. Elle le deviendra lorsque l'utilisateur U2 aura valid sa transaction. Les quatres niveaux SQL d'isolation des transactions ne sont pas disponibles dans tous les SGBD. Le niveau d'tanchit par dfaut est en gnral le niveau Committed Read. Le niveau d'tanchit dsir pour une transaction peut tre indiqu explicitement lors de la cration d'une transaction explicite par un client .NET.

7.6.2

L'API de gestion des transactions

Une connexion implmente l'interface IDbConnection prsente page 225. Cette interface la mthode suivante :
BeginTransaction

M dmarre une transaction.

Cette mthode a deux signatures : 1. 2. IDbTransaction BeginTransaction() : dmarre une transaction et rend l'objet IDbTransaction permettant de la contrler IDbTransaction BeginTransaction(IsolationLevel level) : prcise de plus le niveau d'tanchit dsir pour la transaction. level prend ses valeurs dans l'numration suivante :
ReadUncommitted ReadCommitted

la transaction peut lire des donnes crites par une autre transaction que celle-ci n'a pas encore valides - viter la transaction ne peut pas lire des donnes crites par une autre transaction que celle-ci n'a pas encore valides. Les donnes lues deux fois de suite dans la transaction peuvent cependant changer (not repeatable reads) car une autre transaction a pu les modifier entre-temps (les lignes lues ne sont pas verrouilles - seules les lignes mises jour le sont). Par ailleurs, une autre transaction a pu ajouter des lignes (lignes fantmes) qui seront intgres dans la seconde lecture. les lignes lues par la transaction sont verrouilles l'instar des lignes mises jour. Cela empche une autre transaction de les modifier. Cela n'vite pas l'ajout de lignes. les tables exploites par la transaction sont verrouilles empchant l'ajout de nouvelles lignes par une autre transaction. Tout se passe comme si la transaction tait seule. Diminue les performances car les transactions ne travaillent plus en parallle. la transaction travaille sur une copie des donnes faite au temps T. Utilise lorsque la transaction est en lecture seule. Donne le mme rsultat que serializable en vitant son cot.

RepeatableRead Serializable

Snapshot

Une fois la transaction dmarre, elle est contrle par l'objet de type IDbTransaction, une interface dont nous utiliserons les proprits P et mthodes M suivantes : Nom
Connection Commit Rollback

Type P M M

Rle la connexion IDbConnection qui supporte la transaction valide la transaction - les rsultats des ordres SQL mis dans la transaction sont copis dans la base. invalide la transaction - les rsultats des ordres SQL mis dans la transaction ne sont pas copis dans la base.

Accs aux bases de donnes

254

7.6.3

Le programme exemple

Nous reprenons le projet prcdent pour nous intresser maintenant au programme [Transactions.cs] : 1

en [1], le projet. en [2], le projet est configur pour excuter [Transactions.cs]

Le code de [Transactions.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. using using using using using System; System.Configuration; System.Data; System.Data.SqlServerCe; System.Text;

namespace Chap7 { class Transactions { static void Main(string[] args) { // exploitation du fichier de configuration string connectionString = null; try { // chane de connexion connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString; } catch (Exception e) { Console.WriteLine("Erreur de configuration : {0}", e.Message); return; } // affichages Console.WriteLine("Chane de connexion la base : [{0}]\n", connectionString); // cration d'un tableau de 2 articles de mme nom Article[] articles = new Article[2]; for (int i = 1; i <= articles.Length; i++) { articles[i - 1] = new Article(0, "article", i * 100, i * 10, i); } // on gre les ventuelles exceptions try { Console.WriteLine("Insertion sans transaction..."); // on insre le tableau des articles dans la base d'abord sans transaction ExecuteUpdate(connectionString, "delete from articles"); try { InsertArticlesOutOfTransaction(connectionString, articles); } catch (Exception ex) { // msg d'erreur Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); } ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles");

// on refait la mme chose mais dans une transaction cette fois Console.WriteLine("\n\nInsertion dans une transaction..."); ExecuteUpdate(connectionString, "delete from articles"); InsertArticlesInTransaction(connectionString, articles); ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles"); 47. } catch (Exception ex) { 48. // msg d'erreur

Accs aux bases de donnes

255

49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79.

Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); } }

// insertion d'un tableau d'articles sans transaction static void InsertArticlesOutOfTransaction(string connectionString, Article[] articles) { .... } // insertion d'un tableau d'articles dans une transaction static void InsertArticlesInTransaction(string connectionString, Article[] articles) { .... } // excution d'une requte de mise jour static void ExecuteUpdate(string connectionString, string requte) { .... } // excution d'une requte Select static void ExecuteSelect(string connectionString, string requte) { ... } // affichage reader static void AfficheReader(IDataReader reader) { } } } }

...

lignes 12-19 : la chane de connexion la base SQLServer Ce est lue dans [App.config] lignes 25-28 : un tableau de deux objets Article est cr. Ces deux articles ont le mme nom "article". Or, la base [dbarticles.sdf] a une contrainte d'unicit sur sa colonne [nom] (cf page 222). Donc ces deux articles ne peuvent tre prsents en mme temps dans la base. Les deux articles de nom "article" sont ajouts dans la table articles. Il va donc y avoir un problme, c.a.d. une exception lance par le SGBD et relaye par son connecteur ADO.NET. Pour montrer l'effet de la transaction, les deux articles vont tre insrs dans deux environnements diffrents : d'abord en-dehors de toute transaction. Il faut se rappeler ici que, dans ce cas, SQLServer Compact travaille en mode autocommit, c.a.d. insre chaque ordre SQL dans une transaction implicite. Le 1er article va tre insr. Le second ne le sera pas. ensuite dans une transaction explicite encapsulant les deux insertions. Parce que la deuxime insertion va chouer, la premire sera alors dfaite. Au final aucune insertion ne sera faite. ligne 33 : la table articles est vide ligne 35 : l'insertion des deux articles sans transaction explicite. Parce qu'on sait que la deuxime insertion va provoquer une exception, celle-ci est gre par un try / catch ligne 46 : affichage de la table articles lignes 44-46 : on refait la mme squence mais cette fois ci une transaction explicite est utilise pour faire les insertions. L'exception qui est rencontre est ici gre par la mthode InsertArticlesInTransaction. lignes 54-56 : la mthode InsertArticlesOutOfTransaction est la mthode InsertArticles du programme [Parametres.cs] tudi prcdemment. lignes 64-66 : la mthode ExecuteUpdate est la mme que prcdemment. L'ordre SQL excut l'est dans une transaction implicite. C'est possible ici car on sait que dans ce cas, SQLServer Compact travaille en mode autocommit. lignes 69-71 : idem pour la mthode ExecuteSelect.

La mthode InsertArticlesInTransaction est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. // insertion d'un tableau d'articles dans une transaction static void InsertArticlesInTransaction(string connectionString, Article[] articles) { using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) { // ouverture connexion connexion.Open(); // configuration commande string requte = "insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"; SqlCeCommand sqlCommand = new SqlCeCommand(requte, connexion); sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30); sqlCommand.Parameters.Add("@prix", SqlDbType.Money); sqlCommand.Parameters.Add("@sa", SqlDbType.Int); sqlCommand.Parameters.Add("@sm", SqlDbType.Int); // compilation de la commande sqlCommand.Prepare();

Accs aux bases de donnes

256

15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41.

// transaction SqlCeTransaction transaction = null; try { // dbut transaction transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted); // la commande SQL doit tre excute dans cette transaction sqlCommand.Transaction = transaction; // insertion des lignes for (int i = 0; i < articles.Length; i++) { // initialisation paramtres sqlCommand.Parameters["@nom"].Value = articles[i].Nom; sqlCommand.Parameters["@prix"].Value = articles[i].Prix; sqlCommand.Parameters["@sa"].Value = articles[i].StockActuel; sqlCommand.Parameters["@sm"].Value = articles[i].StockMinimum; // excution requte sqlCommand.ExecuteNonQuery(); } // on valide la transaction transaction.Commit(); Console.WriteLine("transaction valide..."); } catch { // on dfait la transaction if (transaction != null)transaction.Rollback(); Console.WriteLine("transaction invalide..."); } } }

Nous ne dtaillons que ce qui la diffrencie de la mthode InsertArticles du programme [Parametres.cs] tudi prcdemment :

ligne 16 : une transaction SqlCeTransaction est dclare. lignes 17, 35 : le try / catch pour grer l'exception qui va surgir l'issue de la 2ime insertion ligne 19 : la transaction est cre. Elle appartient la connexion courante. ligne 21 : la commande SQL paramtre est mise dans la transaction lignes 23-31 : les insertions sont faites ligne 33 : tout s'est bien pass - la transaction est valide - les insertions vont tre dfinitivement intgres la base de donnes. ligne 37 : on a eu un problme. La transaction est dfaite si elle existait.

L'excution donne les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. Chane de connexion la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;] Insertion sans transaction... Il y a eu 0 ligne(s) modifie(s) Erreur d'accs la base de donnes (A duplicate value cannot be inserted into a unique index. [ Table name = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ]) -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------126 article 100 10 1 Insertion dans une transaction... Il y a eu 1 ligne(s) modifie(s) transaction invalide... -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM ------------------------------------

ligne 4 : affiche par le ExecuteUpdate("delete from articles") - il n'y avait pas de lignes dans la table ligne 5 : l'exception provoque par la deuxime insertion. Le message indique que la contrainte UQ__ARTICLES__0000000000000010 n'a pas t vrifie. On peut en savoir plus en regardant les proprits de la base :

Accs aux bases de donnes

257

1 2 3

en [1] dans la vue [Database Explorer] de Visual Studio, on a cr une connexion [2] sur la base [dbarticles.sdf]. Celle-ci a un index UQ__ARTICLES__0000000000000010. En cliquant droit sur cet index, on a accs ses proprits (Index properties) en [3,4], on voit que l' index UQ__ARTICLES__0000000000000010 corespond une contrainte d'unicit sur la colonne [NOM] lignes 7-11 : affichage de la table articles aprs les deux insertions. Elle n'est pas vide : le 1er article a t insr. ligne 15 : affiche par le ExecuteUpdate("delete from articles") - il y avait une ligne dans la table ligne 16 : message affich par InsertArticlesInTransaction lorsque la transaction choue. lignes 18-20 : montrent qu'aucune insertion n'a t faite. Le Rollback de la transaction a dfait la 1re insertion.

7.7

La mthode ExecuteScalar

Parmi les mthodes de l'interface IDbCommand dcrite page 225, il y avait la mthode suivante :
ExecuteScalar

M pour excuter un ordre SQL Select ne rendant qu'un unique rsultat comme dans : select count(*) from articles.

Nous montrons ici un exemple d'utilisation de cette mthode. Revenons au projet : 1

en [1], le projet. en [2], le projet est configur pour excuter [ExecuteScalar.cs]

Le programme [ExecuteScalar.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. ... namespace Chap7 { class Scalar { static void Main(string[] args) { // exploitation du fichier de configuration string connectionString = null;

Accs aux bases de donnes

258

8. ... 9. 10. // affichages 11. Console.WriteLine("Chane de connexion la base : [{0}]\n", connectionString); 12. 13. // cration d'un tableau de 5 articles 14. Article[] articles = new Article[5]; 15. for (int i = 1; i <= articles.Length; i++) { 16. articles[i - 1] = new Article(0, "article" + i, i * 100, i * 10, i); 17. } 18. 19. // on gre les ventuelles exceptions 20. try { 21. // on insre le tableau des articles dans une transaction 22. ExecuteUpdate(connectionString, "delete from articles"); 23. InsertArticlesInTransaction(connectionString, articles); 24. ExecuteSelect(connectionString, "select id,nom,prix,stockactuel,stockminimum from articles"); 25. // on calcule la moyenne des prix des articles 26. decimal prixMoyen = (decimal)ExecuteScalar(connectionString, "select avg(prix) from articles"); 27. Console.WriteLine("Prix moyen des articles={0}", prixMoyen); 28. // ou le nombre des articles 29. int nbArticles = (int)ExecuteScalar(connectionString, "select count(id) from articles"); 30. Console.WriteLine("Nombre d'articles={0}", nbArticles); 31. } catch (Exception ex) { 32. // msg d'erreur 33. Console.WriteLine("Erreur d'accs la base de donnes (" + ex.Message + ")"); 34. } 35. } 36. 37. // insertion d'un tableau d'articles dans une transaction 38. static void InsertArticlesInTransaction(string connectionString, Article[] articles) { 39. ... 40. } 41. 42. 43. // excution d'une requte de mise jour 44. static object ExecuteScalar(string connectionString, string requte) { 45. using (SqlCeConnection connexion = new SqlCeConnection(connectionString)) { 46. // ouverture connexion 47. connexion.Open(); 48. // excution requte 49. return new SqlCeCommand(requte, connexion).ExecuteScalar(); 50. } 51. } 52. 53. // excution d'une requte de mise jour 54. static void ExecuteUpdate(string connectionString, string requte) { 55. ... 56. } 57. 58. // excution d'une requte Select 59. static void ExecuteSelect(string connectionString, string requte) { 60. ... 61. } 62. 63. // affichage reader 64. static void AfficheReader(IDataReader reader) { 65. ... 66. } 67. } 68. }

lignes 14-17 : cration d'un tableau de 5 articles ligne 22 : la table articles est vide ligne 23 : elle est remplie avec les 5 acticles ligne 24 : elle est affiche ligne 26 : demande le prix moyen des articles ligne 29 : demande le nombre d'articles ligne 49 : utilisation de la mthode [IDbCommand].ExecuteScalar() pour calculer chacune de ces valeurs.

Les rsultats de l'excution sont les suivants :


1. 2. Chane de connexion la base : [Data Source=|DataDirectory|\dbarticles.sdf;Password=dbarticles;]

Accs aux bases de donnes

259

3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16.

Il y a eu 5 ligne(s) modifie(s) transaction valide... -----------------------------------ID,NOM,PRIX,STOCKACTUEL,STOCKMINIMUM -----------------------------------145 article1 100 10 1 146 article2 200 20 2 147 article3 300 30 3 148 article4 400 40 4 149 article5 500 50 5 Prix moyen des articles=300 Nombre d'articles=5

Les lignes 15 et 16 montrent les deux valeurs renvoyes par la mthode ExecuteScalar.

7.8

Application exemple - version 7

On reprend l'application exemple IMPOTS. La dernire version a t tudie au paragraphe 5.6, page 201. C'tait l'application trois couches suivante :

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL 3 2 SPRING

Couche d'accs aux donnes [dao] DLL 1

Donnes

la couche [ui] tait une interface graphique [A] et la couche [dao] trouvait ses donnes dans un fichier texte [B]. l'instanciation des couches et leur intgration dans l'application taient assures par Spring. A B

Nous modifions la couche [dao] afin qu'elle aille chercher ses donnes dans une base de donnes.

7.8.1

La base de donnes

Le contenu du fichier texte [B] prcdent est mis dans une base de donnes MySQL5. Nous montrons comment procder :

Accs aux bases de donnes

260

4 2 3

[1] :MySQL Administrator a t lanc [2,3] : dans la zone [Schemata], cliquer droit et prendre l'option [Create Schema] pour crer une nouvelle base [4] : la base s'appellera [bdimpots] [5] : elle a t ajoute aux bases de la zone [Schemata].

11

9 7 10 12

[6,7] : cliquer droit sur la table et prendre l'option [Create New Table] pour crer une table [8] : la table s'appellera [tranches]. Elle aura les colonnes [id, limite, coeffR, coeffN]. [9,10] : [id] est cl primaire de type INTEGER et a l'attribut AUTO_INCREMENT [10] : c'est le SGBD qui se chargera de remplir cette colonne lors d'ajout de lignes. les colonnes [limite, coeffR, coeffN] sont de type DOUBLE. [11,12] : la nouvelle table apparat dans l'onglet [Schema Tables] de la base de donnes.

15 13 14 16

17

[13,14] : pour mettre des donnes dans la table [15] : [Query Browser] a t lanc [16] : les donnes ont t entres et valides pour les colonnes [limite, coeffR, coeffN]. La colonne [id] a t remplie par le SGBD. La validation a eu lieu avec [17].

Accs aux bases de donnes

261

18 20 19

toujours dans [Query Browser] [18], on excute [20] la requte [19]. Celle-ci cre un utilisateur 'admimpots' de mot de passe 'mdpimpots' et lui donne tous les privilges (grant all privileges) sur tous les objets de la base bdimpots (on bdimpots.*). Cela va nous permettre de travailler sur la base [bdimpots] avec l'utilisateur [admimpots] plutt qu'avec l'administrateur [root].

7.8.2

La solution Visual Studio

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

Nous suivrons la dmarche tudie pour la version 5 de l'application exemple (cf paragraphe 4.4, page 138). Nous allons construire progressivement la solution Visual Studio suivante :

3B 4b 1 3 4

en [1] : la solution ImpotsV7 est forme de trois projets, un pour chacune des trois couches de l'application en [2] : le projet [dao] de la couche [dao] qui va dsormais exploiter une base de donnes en [3] : le projet [metier] de la couche [metier]. Nous reprenons ici la couche [metier] de la version 5, dcrite au paragraphe 4.4.4, page 152. en [4] : le projet [ui] de la couche [ui]. Nous reprenons ici la couche [ui] de la version 6, dcrite au paragraphe 5.6, page 201.

Nous nous appuyons sur l'acquis pour rcuprer deux couches dj crites, les couches [ui] et [metier]. Cela est rendu possible par l'architecture en couches choisie. Nous aurons nanmoins besoin des codes source des couches [ui] et [metier]. Il n'est en effet pas possible de se contenter des DLL des couches. Lorsque dans la version 5, la DLL de la couche [metier] a t cre, elle avait une dpendance sur la DLL de la couche [dao]. Cette dpendance a t inscrite en dur dans la DLL de la couche [metier] (nom de la DLL de la couche [dao], version, jeton d'identit, ...). Ainsi la DLL de la version 5 [ImpotsV5-metier.dll] n'accepte de travailler qu'avec la DLL [ImpotsV5-dao.dll] avec laquelle elle a t compile. Si on change la DLL de la couche [dao] il faut recompiler la

Accs aux bases de donnes

262

couche [metier] pour lui crer une nouvelle DLL. Il en est de mme pour la couche [ui]. Les couches [ui] et [metier] ne seront donc pas modifies mais elles seront recompiles pour travailler avec la DLL de la nouvelle couche [dao].

7.8.3

La couche [dao]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

3 2 4

Les rfrences du projet (cf [1] dans le projet)


nunit.framework : pour le test NUnit System.Configuration : pour exploiter le fichier de configuration [App.config] System.Data : parce qu'on exploite une base de donnes.

Les entits (cf [2] dans le projet) Les classes [TrancheImpot] et [ImpotException] sont celles des versions prcdentes. La couche [dao] (cf [3] dans le projet) L'interface [IImpotDao] n'a pas chang :
1. 2. 3. 4. 5. 6. 7. 8. using Entites; namespace Dao { public interface IImpotDao { // les tranches d'impt TrancheImpot[] TranchesImpot{get;} } }

La classe d'implmentation [DataBaseImpot] de cette interface est la suivante :


1. 2. 3. using System; using System.Collections.Generic; using System.Data.Common;

Accs aux bases de donnes

263

4. using Entites; 5. 6. namespace Dao { 7. public class DataBaseImpot : IImpotDao { 8. // tranches d'impt 9. private TrancheImpot[] tranchesImpot; 10. public TrancheImpot[] TranchesImpot { get { return tranchesImpot; } } 11. 12. // constructeur 13. public DataBaseImpot(string factory, string connectionString, string requte) { 14. // factory : la factory du SGBD cible 15. // connectionString : la chane de connexion la base des tranches d'impot 16. // on gre les ventuelles exceptions 17. try { 18. // on rcupre un connecteur gnrique pour le SGBD 19. DbProviderFactory connecteur = DbProviderFactories.GetFactory(factory); 20. using (DbConnection connexion = connecteur.CreateConnection()) { 21. // configuration connexion 22. connexion.ConnectionString = connectionString; 23. // ouverture connexion 24. connexion.Open(); 25. // configuration Command 26. DbCommand sqlCommand = connecteur.CreateCommand(); 27. sqlCommand.CommandText = requte; 28. sqlCommand.Connection = connexion; 29. // excution requte 30. List<TrancheImpot> listTrancheImpot = new List<TrancheImpot>(); 31. using (DbDataReader reader = sqlCommand.ExecuteReader()) { 32. while (reader.Read()) { 33. // on cre une nouvelle trance d'impt 34. listTrancheImpot.Add(new TrancheImpot() { Limite = reader.GetDecimal(0), CoeffR = reader.GetDecimal(1), CoeffN = reader.GetDecimal(2) }); 35. } 36. } 37. // on met les tranches d'impt dans son instance 38. tranchesImpot = listTrancheImpot.ToArray(); 39. } 40. } catch (Exception ex) { 41. // on encapsule l'exception dans un type ImpotException 42. throw new ImpotException("Erreur de lecture des tranches d'impt", ex) { Code = 101 }; 43. } 44. 45. } 46. } 47. }

ligne 7 : la classe [DataBaseImpot] implmente l'interface [IImpotDao]. ligne 10 : l'implmentation de la mthode [TranchesImpot] de l'interface. Elle se contente de rendre une rfrence sur le tableau des tranches d'impt de la ligne 9. Ce tableau va tre construit par le constructeur de la classe. ligne 13 : le constructeur. Il utilise un connecteur gnrique (cf paragraphe 7.4.5, page 242) pour exploiter la base de donnes des tranches d'impt. Le constructeur reoit trois paramtres : 1. le nom de la "factory" auprs de laquelle il va demander les classes pour se connecter la base, mettre des ordres SQL, exploiter le rsultat d'un Select. 2. la chane de connexion qu'il doit utiliser pour se connecter la base de donnes 3. l'ordre SQL Select qu'il doit exccuter pour avoir les tranches d'impt. ligne 19 : demande un connecteur la "factory" ligne 20 : cre une connexion avec ce connecteur. Elle est cre mais pas encore oprationnelle ligne 22 : la chane de connexion de la connexion est initialise. On peut dsormais se connecter. ligne 24 : on se connecte ligne 26 : demande au connecteur, un objet [DbCommand] pour excuter un ordre SQL ligne 27 : fixe l'ordre SQL excuter ligne 28 : fixe la connexion sur laquelle l'excuter ligne 30 : une liste [listTrancheImpot] d'objets de type [TrancheImpot] est cre vide. ligne 31 : l'ordre SQL Select est excut lignes 32-35 : l'objet [DbDataReader] rsultat du Select est exploit. Chaque ligne de la table rsultat du Select sert instancier un objet de type [TrancheImpot] qui est ajout la liste [listTrancheImpot]. ligne 38 : la liste d'objets de type [TrancheImpot] est transfre dans le tableau de la ligne 9. lignes 40-43 : une ventuelle exception est encapsule dans un type [ImpotException] et se voit attribuer le code d'erreur 101 (arbitraire).

Le test [Test1] (cf [4] dans le projet)

Accs aux bases de donnes

264

La classe [Test1] se contente d'afficher les tranches d'impt l'cran. C'est celle dj utilise dans la version 5 (page 147) sauf pour l'instruction qui instancie la couche [dao] (ligne 14).
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. using using using using System; Dao; Entites; System.Configuration;

namespace Tests { class Test1 { static void Main() { // on cre la couche [dao] IImpotDao dao = null; try { // cration couche [dao] dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]); } catch (ImpotException e) { // affichage erreur string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message); Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg); // arrt programme Environment.Exit(1); } // on affiche les tranches d'impt TrancheImpot[] tranchesImpot = dao.TranchesImpot; foreach (TrancheImpot t in tranchesImpot) { Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN); } } } }

La ligne 14 exploite le fichier de configuration [App.config] suivant :


1. 2. 3. 4. <?xml version="1.0" encoding="utf-8" ?> <configuration> <connectionStrings> <add name="dbImpotsMySql5" connectionString="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;" /> 5. </connectionStrings> 6. <appSettings> 7. <add key="requete" value="select limite, coeffr, coeffn from tranches"/> 8. <add key="factoryMySql5" value="MySql.Data.MySqlClient"/> 9. </appSettings> 10. </configuration>

ligne 4 : la chane de connexion la base MySQL5. On notera que c'est l'utilisateur [admimpots] qui tablira la connexion. ligne 8 : la "factory" pour travailler avec le SGBD MySQL5 ligne 7 : la requte SQL Select pour obtenir les tranches d'impt.

Le projet est configur pour excuter [Test1.cs] :

L'excution du test donne les rsultats suivants :


1. 2. 3. 4962:0:0 8382:0,068:291,09 14753:0,191:1322,92

Accs aux bases de donnes

265

4. 5. 6. 7.

23888:0,283:2668,39 38868:0,374:4846,98 47932:0,426:6883,66 0:0,481:9505,54

Le test NUnit [NUnit1] (cf [4] dans le projet) Le test unitaire [NUnit1] est celui dj utilis dans la version 5 (page 147) sauf pour l'instruction qui instancie la couche [dao] (ligne 16).
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. using using using using using System; System.Configuration; Dao; Entites; NUnit.Framework;

namespace Tests { [TestFixture] public class NUnit1 : AssertionHelper{ // couche [dao] tester private IImpotDao dao;

// constructeur public NUnit1() { // initialisation couche [dao] dao = new DataBaseImpot(ConfigurationManager.AppSettings["factoryMySql5"], ConfigurationManager.ConnectionStrings["dbImpotsMySql5"].ConnectionString, ConfigurationManager.AppSettings["requete"]); 17. } 18. 19. // test 20. [Test] 21. public void ShowTranchesImpot(){ 22. // on affiche les tranches d'impt 23. TrancheImpot[] tranchesImpot = dao.TranchesImpot; 24. foreach (TrancheImpot t in tranchesImpot) { 25. Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN); 26. } 27. // qqs tests 28. Expect(tranchesImpot.Length,EqualTo(7)); 29. Expect(tranchesImpot[2].Limite,EqualTo(14753).Within(1e-6)); 30. Expect(tranchesImpot[2].CoeffR, EqualTo(0.191).Within(1e-6)); 31. Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92).Within(1e-6)); 32. } 33. } 34. }

Pour excuter ce test unitaire, le projet doit tre de type [Class Library] :

2 1 3

en [1] : la nature du projet a t change en [2] : la DLL gnre s'appellera [ImpotsV7-dao.dll] en [3] : aprs gnration (F6) du projet, le dossier [dao/bin/Release] contient la DLL [ImpotsV7-dao.dll]. Il contient aussi le fichier de configuration [App.config] renomm [nom DLL].config. C'est standard dans Visual studio.

La DLL [ImpotsV7-dao.dll] est ensuite charge dans le framework NUnit et excute :

Accs aux bases de donnes

266

2 1 3

en [1] : les tests ont t russis. Nous considrons dsormais la couche [dao] oprationnelle. Sa DLL contient toutes les classes du projet dont les classes de test. Celles-ci sont inutiles. Nous reconstruisons la DLL afin d'en exclure les classes de tests. en [2] : le dossier [tests] est exclu du projet en [3] : le nouveau projet. Celui-ci est rgnr par F6 afin de gnrer une nouvelle DLL. C'est cette DLL qui sera utilise par les couches [metier] et [ui] de l'application.

7.8.3.1

La couche [metier]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

2 1 3

en [1], le projet [metier] est devenu le projet actif de la solution en [2] : les rfrences du projet. On notera la rfrence sur la DLL de la couche [dao] cre prcdemment. Cette procdure d'ajout de rfrence a t dcrite dans la version 5, au paragraphe 4.4.4, page 152. en [3] : la couche [metier]. C'est celle de la version 5, dcrite au paragraphe 4.4.4, page 152.

Le projet [metier] est configur pour gnrer une DLL :

Accs aux bases de donnes

267

[1] : le projet est de type "bibliothque de classes" [2] : la gnration du projet produira la DLL [ImpotsV7-metier.dll] [3].

Le projet est gnr (F6).

7.8.4

La couche [ui]

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

2 1

4 3

en [1], le projet [ui] est devenu le projet actif de la solution en [2] : les rfrences du projet. On notera les rfrences sur les DLL des couches [dao] et [metier]. en [3] : la couche [ui]. C'est celle de la version 6 dcrite au paragraphe 5.6, page 201. en [4], le fichier de configuration [App.config] est analogue celui de la version 6. Il n'en diffre que par la faon dont la couche [dao] est instancie par Spring :
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" /> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" /> </sectionGroup> </configSections> <spring> <context> <resource uri="config://spring/objects" /> </context> <objects xmlns="http://www.springframework.net"> <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao"> <constructor-arg index="0" value="MySql.Data.MySqlClient"/> <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/> <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/> </object> <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier"> <constructor-arg index="0" ref="dao"/> </object>

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23.

Accs aux bases de donnes

268

24. </objects> 25. </spring> 26. </configuration>

lignes 11-25 : la configuration Spring lignes 15-24 : les objets instancis par Spring lignes 16-20 : instanciation de la couche [dao] ligne 16 : la couche [dao] est instancie par la classe [Dao.DataBaseImpot] qui se trouve dans la DLL [ImpotsV7-Dao] lignes 17-19 : les trois paramtres (factory du SGBD utilis, chane de connexion, requte SQL) fournir au constructeur de la classe [Dao.DataBaseImpot] lignes 21-23 : instanciation de la couche [metier]. C'est la mme configuration que dans la version 6.

Tests Le projet [ui] est configur comme suit :

2 3 1

[1] : le projet est de type "Windows Application" [2] : la gnration du projet produira l'excutable [ImpotsV7-ui.exe]

Un exemple d'excution est donn en [3].

7.8.5

Changer la base de donnes

utilisateur

Couche ui [ui]

Couche mtier [metier] DLL Couche [entites] SPRING

Couche d'accs aux donnes [dao] DLL

Donnes

La couche [dao] ci-dessus a t crite avec un connecteur gnrique et une base MySQL5. Nous nous proposons ici de passer une base SQL Server Compact afin de montrer que seule la configuration va changer. La base SQL Server Compact sera la suivante :

Accs aux bases de donnes

269

2 3 1 4 5

[1] : la base [dbimpots.sdf] dans la vue [DataBase Explorer] de Visual studio [2]. Elle a t cre sans mot de passe. [3] : la table [data] qui contient les donnes. On a volontairement choisi des noms diffrents pour la table et les colonnes de ceux utiliss avec la base MySQL5 afin d'insister de nouveau sur l'intrt de mettre ce genre de dtails dans le fichier de configuration plutt que dans le code. [4] : la colonne [id] est cl primaire et a l'attribut Identity : c'est le SGBD qui va lui attribuer ses valeurs. [5] : le contenu de la table [data].

[6] : la base [dbimpots.sdf] a t place dans le dossier du projet [ui] et intgre ce projet. [7] : la base [dbimpots.sdf] sera copie dans le dossier d'excution du projet.

Le fichier de configuration [App.config] pour la nouvelle base de donnes est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" /> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" /> </sectionGroup> </configSections> <spring> <context> <resource uri="config://spring/objects" /> </context> <objects xmlns="http://www.springframework.net"> <!-<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao"> <constructor-arg index="0" value="MySql.Data.MySqlClient"/> <constructor-arg index="1" value="Server=localhost;Database=bdimpots;Uid=admimpots;Pwd=mdpimpots;"/> <constructor-arg index="2" value="select limite, coeffr, coeffn from tranches"/> </object> --> <object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">

Accs aux bases de donnes

270

24. <constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/> 25. <constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" /> 26. <constructor-arg index="2" value="select data1, data2, data3 from data"/> 27. </object> 28. <object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier"> 29. <constructor-arg index="0" ref="dao"/> 30. </object> 31. </objects> 32. </spring> 33. </configuration>

lignes 23-27 : la configuration de la couche [dao] pour exploiter la base [dbimpots.sdf].

Les rsultats de l'excution sont identiques aux prcdents. On notera l'intrt d'utiliser un connecteur gnrique pour rendre la couche [dao] insensible au changement de SGBD. Nous avons vu cependant que ce connecteur ne convenait pas toutes les situations, notamment celles o des requtes paramtres sont utilises. Il y a alors d'autres solutions telle celle voque, des frameworks tiers d'accs aux donnes (Spring, iBatis, NHibernate, LINQ, ...).

7.9

Pour aller plus loin ...


LINQ est prsent dans de nombreux ouvrages, notamment dans le livre : C# 3.0 in a Nutshell, Joseph et Ben Albahari, ditions O'Reilly dj cit dans l'introduction de ce document. iBatis est prsent dans le livre : iBatis in Action, Clinton Begin, ditions Manning Nhibernate in Action aux ditions Manning est prvu pour juillet 2008

Spring, iBatis, NHibernate ont des manuels de rfrence disponibles sur le site de ces diffrents frameworks.

Accs aux bases de donnes

271

8
8.1

Les threads d'excution


La classe Thread

Lorsqu'on lance une application, elle s'excute dans un flux d'excution appel un thread. La classe .NET modlisant un thread est la classe System.Threading.Thread et a la dfinition suivante : Constructeurs

1 3

Nous n'utiliserons dans les exemples suivre que les constructeurs [1,3]. Le constructeur [1] admet comme paramtre une mthode ayant la signature [2], c.a.d. ayant un paramtre de type object et ne rendant pas de rsultat. Le constructeur [3] admet comme paramtre une mthode ayant la signature [4], c.a.d. n'ayant pas de paramtre et ne rendant pas de rsultat. Proprits Quelques proprits utiles : Thread CurrentThread : proprit statique qui donne une rfrence sur le thread dans lequel se trouve le code ayant demand cette proprit string Name : le nom du thread bool IsAlive : indique si le thread est en cours d'excution ou non. Mthodes Les mthodes les plus utilises sont les suivantes : Start(), Start(object obj) : lance l'excution asynchrone du thread, ventuellement en lui passant de l'information dans un type object. Abort(), Abort(object obj) : pour terminer de force un thread Join() : le thread T1 qui excute T2.Join est bloqu jusqu' ce que soit termin le thread T2. Il existe des variantes pour terminer l'attente au bout d'un temps dtermin. Sleep(int n) : mthode statique - le thread excutant la mthode est suspendu pendant n millisecondes. Il perd alors le processeur qui est donn un autre thread. Regardons une premire application mettant en vidence l'existence d'un thread principal d'excution, celui dans lequel s'excute la fonction Main d'une classe :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. using System; using System.Threading; namespace Chap8 { class Program { static void Main(string[] args) { // init thread courant Thread main = Thread.CurrentThread; // affichage Console.WriteLine("Thread courant : {0}", main.Name); // on change le nom main.Name = "main"; // vrification Console.WriteLine("Thread courant : {0}", main.Name); // boucle infinie while (true) { // affichage

Les threads d'excution

272

19. 20. 21. 22. 23. 24. } 25. }

Console.WriteLine("{0} : {1:hh:mm:ss}", main.Name, DateTime.Now); // arrt temporaire Thread.Sleep(1000); }//while

ligne 8 : on rcupre une rfrence sur le thread dans lequel s'excute la mthode [main] lignes 10-14 : on affiche et on modifie son nom lignes 17-22 : une boucle qui fait un affichage toutes les secondes ligne 21 : le thread dans lequel s'excute la mthode [main] va tre suspendu pendant 1 seconde

Les rsultats cran sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. Thread courant : Thread courant : main main : 04:19:00 main : 04:19:01 main : 04:19:02 main : 04:19:03 main : 04:19:04 ^CAppuyez sur une touche pour continuer...

ligne 1 : le thread courant n'avait pas de nom ligne 2 : il en a un lignes 3-7 : l'affichage qui a lieu toutes les secondes ligne 8 : le programme est interrompu par Ctrl-C.

8.2

Cration de threads d'excution

Il est possible d'avoir des applications o des morceaux de code s'excutent de faon "simultane" dans diffrents threads d'excution. Lorsqu'on dit que des threads s'excutent de faon simultane, on commet souvent un abus de langage. Si la machine n'a qu'un processeur comme c'est encore souvent le cas, les threads se partagent ce processeur : ils en disposent, chacun leur tour, pendant un court instant (quelques millisecondes). C'est ce qui donne l'illusion du paralllisme d'excution. La portion de temps accorde un thread dpend de divers facteurs dont sa priorit qui a une valeur par dfaut mais qui peut tre fixe galement par programmation. Lorsqu'un thread dispose du processeur, il l'utilise normalement pendant tout le temps qui lui a t accord. Cependant, il peut le librer avant terme : en se mettant en attente d'un vnement (Wait, Join) en se mettant en sommeil pendant un temps dtermin (Sleep) 1. Un thread T est tout d'abord cr par l'un des constructeurs prsents plus haut, par exemple :
Thread thread=new Thread(Start);

o Start est une mthode ayant l'une des deux signatures suivantes :
void Start(); void Start(object obj);

2.

3. 4. 5.

La cration d'un thread ne lance pas celui-ci. L'excution du thread T est lanc par T.Start() : la mthode Start passe au constructeur de T va alors tre excute par le thread T. Le programme qui excute l'instruction T.Start() n'attend pas la fin de la tche T : il passe aussitt l'instruction qui suit. On a alors deux tches qui s'excutent en parallle. Elles doivent souvent pouvoir communiquer entre elles pour savoir o en est le travail commun raliser. C'est le problme de synchronisation des threads. Une fois lanc, le thread T s'excute de faon autonome. Il s'arrtera lorsque la mthode Start qu'il excute aura fini son travail. On peut forcer le thread T se terminer : a. T.Abort() demande au thread T de se terminer. On peut aussi attendre la fin de son excution par T.Join(). On a l une instruction bloquante : le programme qui l'excute est bloqu jusqu' ce que la tche T ait termin son travail. C'est un moyen de synchronisation.

Examinons le programme suivant :


1. using System;

Les threads d'excution

273

2. using System.Threading; 3. 4. namespace Chap8 { 5. class Program { 6. public static void Main() { 7. // init Thread courant 8. Thread main = Thread.CurrentThread; 9. // on fixe un nom au Thread 10. main.Name = "Main"; 11. 12. // cration de threads d'excution 13. Thread[] tches = new Thread[5]; 14. for (int i = 0; i < tches.Length; i++) { 15. // on cre le thread i 16. tches[i] = new Thread(Affiche); 17. // on fixe le nom du thread 18. tches[i].Name = i.ToString(); 19. // on lance l'excution du thread i 20. tches[i].Start(); 21. } 22. 23. // fin de main 24. Console.WriteLine("Fin du thread {0} {1:hh:mm:ss}",main.Name,DateTime.Now); 25. } 26. 27. public static void Affiche() { 28. // affichage dbut d'excution 29. Console.WriteLine("Dbut d'excution de la mthode Affiche dans le Thread {0} : {1:hh:mm:ss}",Thread.CurrentThread.Name,DateTime.Now); 30. // mise en sommeil pendant 1 s 31. Thread.Sleep(1000); 32. // affichage fin d'excution 33. Console.WriteLine("Fin d'excution de la mthode Affiche dans le Thread {0} : {1:hh:mm:ss}", Thread.CurrentThread.Name, DateTime.Now); 34. } 35. } 36. }

lignes 8-10 : on donne un nom au thread qui excute la mthode [Main] lignes 13-21 : on cre 5 threads et on les excute. Les rfrences des threads sont mmorises dans un tableau afin de pouvoir les rcuprer ultrieurement. Chaque thread excute la mthode Affiche des lignes 27-35. ligne 20 : le thread n i est lanc. Cette opration est non bloquante. Le thread n i va s'excuter en parallle du thread de la mthode [Main] qui l'a lanc. ligne 24 : le thread qui excute la mthode [Main] se termine. lignes 27-35 : la mthode [Affiche] fait des affichages. Elle affiche le nom du thread qui l'excute ainsi que les heures de dbut et fin d'excution. ligne 31 : tout thread excutant la mthode [Affiche] va s'arrter pendant 1 seconde. Le processeur va alors tre donn un autre thread en attente de processeur. A la fin de la seconde d'arrt, le thread arrt va tre candidat au processeur. Il l'aura lorsque son tour sera venu. Cela dpend de divers facteurs dont la priorit des autres threads en attente de processeur.

Les rsultats sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Fin du thread Main 10:30:44 Fin d'excution de la mthode Affiche dans le Thread 0 Fin d'excution de la mthode Affiche dans le Thread 1 Fin d'excution de la mthode Affiche dans le Thread 2 Fin d'excution de la mthode Affiche dans le Thread 3 Fin d'excution de la mthode Affiche dans le Thread 4 0 1 2 3 4 : : : : : : : : : : 10:30:44 10:30:44 10:30:44 10:30:44 10:30:44

10:30:45 10:30:45 10:30:45 10:30:45 10:30:45

Ces rsultats sont trs instructifs :

on voit tout d'abord que le lancement de l'excution d'un thread n'est pas bloquante. La mthode Main a lanc l'excution de 5 threads en parallle et a termin son excution avant eux. L'opration
// on lance l'excution du thread i tches[i].Start();

Les threads d'excution

274

lance l'excution du thread tches[i] mais ceci fait, l'excution se poursuit immdiatement avec l'instruction qui suit sans attendre la fin d'excution du thread. tous les threads crs doivent excuter la mthode Affiche. L'ordre d'excution est imprvisible. Mme si dans l'exemple, l'ordre d'excution semble suivre l'ordre des demandes d'excution, on ne peut en conclure de gnralits. Le systme d'exploitation a ici 6 threads et un processeur. Il va distribuer le processeur ces 6 threads selon des rgles qui lui sont propres. on voit dans les rsultats une consquence de la mthode Sleep. Dans l'exemple, c'est le thread 0 qui excute le premier la mthode Affiche. Le message de dbut d'excution est affich puis il excute la mthode Sleep qui le suspend pendant 1 seconde. Il perd alors le processeur qui devient ainsi disponible pour un autre thread. L'exemple montre que c'est le thread 1 qui va l'obtenir. Le thread 1 va suivre le mme parcours ainsi que les autres threads. Lorsque la seconde de sommeil du thread 0 va tre termine, son excution peut reprendre. Le systme lui donne le processeur et il peut terminer l'excution de la mthode Affiche.

Modifions notre programme pour terminer la mthode Main par les instructions :
1. 2. 3. 4. // fin de main Console.WriteLine("Fin du thread " + main.Name); // on arrte tous les threads Environment.Exit(0);

L'excution du nouveau programme donne les rsultats suivants :


1. 2. 3. 4. 5. 6. Dbut d'excution de Dbut d'excution de Dbut d'excution de Dbut d'excution de Dbut d'excution de Fin du thread Main la mthode la mthode la mthode la mthode la mthode 10:33:18 Affiche Affiche Affiche Affiche Affiche dans dans dans dans dans le le le le le Thread Thread Thread Thread Thread 0 1 2 3 4 : : : : : 10:33:18 10:33:18 10:33:18 10:33:18 10:33:18

lignes 1-5 : les threads crs par la fonction Main commencent leur excution et sont interrompus pendant 1 seconde ligne 6 : le thread [Main] rcupre le processeur et excute l'instruction :
Environment.Exit(0);

Cette instruction arrte tous les threads de l'application et non simplement le thread Main. Si la mthode Main veut attendre la fin d'excution des threads qu'elle a crs, elle peut utiliser la mthode Join de la classe Thread :
1. 2. ... 3. 4. 5. 6. 7. 8. 9. 10. } public static void Main() { // on attend tous les threads for (int i = 0; i < tches.Length; i++) { // attente de la fin d'excution du thread i tches[i].Join(); } // fin de main Console.WriteLine("Fin du thread {0} {1:hh:mm:ss}", main.Name, DateTime.Now);

ligne 6 : le thread [Main] attend chacun des threads. Il est d'abord bloqu en attente du thread n 1, puis du thread n 2, etc... Au final lorsqu'il sort de la boucle des lignes 2-5, c'est ce que les 5 threads qu'il a lancs sont finis.

On obtient alors les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Dbut d'excution de la mthode Affiche dans le Thread Fin d'excution de la mthode Affiche dans le Thread 0 Fin d'excution de la mthode Affiche dans le Thread 1 Fin d'excution de la mthode Affiche dans le Thread 2 Fin d'excution de la mthode Affiche dans le Thread 3 Fin d'excution de la mthode Affiche dans le Thread 4 Fin du thread Main 10:35:19 0 1 2 3 4 : : : : : : 10:35:18 : 10:35:18 : 10:35:18 : 10:35:18 : 10:35:18 10:35:19 10:35:19 10:35:19 10:35:19 10:35:19

ligne 11 : le thread [Main] s'est termin aprs les threads qu'il avait lancs.

Les threads d'excution

275

8.3

Intrt des threads

Maintenant que nous avons mis en vidence l'existence d'un thread par dfaut, celui qui excute la mthode Main, et que nous savons comment en crer d'autres, arrtons-nous sur l'intrt pour nous des threads et sur les raisons pour lesquelles nous les prsentons ici. Il y a un type d'applications qui se prtent bien l'utilisation des threads, ce sont les applications client-serveur de l'internet. Nous allons les prsenter dans le chapitre qui suit. Dans une application client-serveur de l'internet, un serveur situ sur une machine S1 rpond aux demandes de clients situs sur des machines distantes C1, C2, ..., Cn. C1 Serveur S1 C2 Cn

Nous utilisons tous les jours des applications de l'internet correspondant ce schma : services Web, messagerie lectronique, consultation de forums, transfert de fichiers... Dans le schma ci-dessus, le serveur S1 doit servir les clients Ci de faon simultane. Si nous prenons l'exemple d'un serveur FTP (File Transfer Protocol) qui dlivre des fichiers ses clients, nous savons qu'un transfert de fichier peut prendre parfois plusieurs minutes. Il est bien sr hors de question qu'un client monopolise tout seul le serveur pendant une telle dure. Ce qui est fait habituellement, c'est que le serveur cre autant de threads d'excution qu'il y a de clients. Chaque thread est alors charg de s'occuper d'un client particulier. Le processeur tant partag cycliquement entre tous les threads actifs de la machine, le serveur passe alors un peu de temps avec chaque client assurant ainsi la simultanit du service. Serveur
thread 1 thread 2

Clients
client 1 client 2

thread n

client n

Dans la pratique, le serveur utilise un pool de threads avec un nombre limit de threads, 50 par exemple. Le 51 ime client est alors pri d'attendre.

8.4

Echange d'informations entre threads

Dans les exemples prcdents, un thread tait initialis de la faon suivante :


Thread t=new Thread(Run);

o Run tait une mthode ayant la signature suivante :


void Run();

Il est galement possible d'utiliser la signature suivante :


void Run(object obj);

Cela permet de transmettre de l'information au thread lanc. Ainsi


t.Start(obj1);

va lancer le thread t qui va alors excuter la mthode Run qui lui a t associe par construction, en lui passant le paramtre effectif obj1. Voici un exemple :
1. using System;

Les threads d'excution

276

2. using System.Threading; 3. 4. namespace Chap8 { 5. class Program4 { 6. public static void Main() { 7. // init Thread courant 8. Thread main = Thread.CurrentThread; 9. // on fixe un nom au Thread 10. main.Name = "Main"; 11. 12. // cration de threads d'excution 13. Thread[] tches = new Thread[5]; 14. Data[] data = new Data[5]; 15. for (int i = 0; i < tches.Length; i++) { 16. // on cre le thread i 17. tches[i] = new Thread(Sleep); 18. // on fixe le nom du thread 19. tches[i].Name = i.ToString(); 20. // on lance l'excution du thread i 21. tches[i].Start(data[i] = new Data { Dbut = DateTime.Now, Dure = i+1 }); 22. } 23. // on attend tous les threads 24. for (int i = 0; i < tches.Length; i++) { 25. // attente de la fin d'excution du thread i 26. tches[i].Join(); 27. // affichage rsultat 28. Console.WriteLine("Thread {0} termin : dbut {1:hh:mm:ss}, dure programme {2} s, fin {3:hh:mm:ss}, dure effective {4}", 29. tches[i].Name,data[i].Dbut,data[i].Dure,data[i].Fin,(data[i].Fin-data[i].Dbut)); 30. } 31. // fin de main 32. Console.WriteLine("Fin du thread {0} {1:hh:mm:ss}", main.Name, DateTime.Now); 33. } 34. 35. public static void Sleep(object infos) { 36. // on rcupre le paramtre 37. Data data = (Data)infos; 38. // mise en sommeil pendant Dure secondes 39. Thread.Sleep(data.Dure*1000); 40. // fin d'excution 41. data.Fin = DateTime.Now; 42. } 43. } 44. 45. internal class Data { 46. // informations diverses 47. public DateTime Dbut { get; set; } 48. public int Dure { get; set; } 49. public DateTime Fin { get; set; } 50. } 51. }

lignes 45-50 : l'information de type [Data] passe aux threads : Dbut : heure du dbut de l'excution du thread - fixe par le thread lanceur Dure : dure en secondes du Sleep excut par le thread lanc - fixe par le thread lanceur Fin : heure du dbut de l'excution du thread - fixe par le thread lanc Il y a l un change d'informations entre le thread lanceur et le thread lanc. lignes 35-43 : la mthode Sleep excute par les threads a la signature void Sleep(object obj). Le paramtre effectif obj sera du type [Data] dfini ligne 45. lignes 15-22 : cration de 5 threads ligne 17 : chaque thread est associ la mthode Sleep de la ligne 35 ligne 21 : un objet de type [Data] est pass la mthode Start qui lance le thread. Dans cet objet on a not l'heure de dbut de l'excution du thread ainsi que la dure en secondes pendant laquelle il doit dormir. Cet objet est mmoris dans le tableau de la ligne 14. lignes 24-30 : le thread [Main] attend la fin de tous les threads qu'il a lancs. lignes 28-29 : le thread [Main] rcupre l'objet data[i] du thread n i et en affiche le contenu. lignes 35-42 : la mthode Sleep excute par les threads ligne 37 : on rcupre le paramtre de type [Data] ligne 39 : le champ Dure du paramtre est utilis pour fixer la dure du Sleep ligne 41 : le champ Fin du paramtre est initialis

Les rsultats de l'excution sont les suivants :

Les threads d'excution

277

1. 2. 3. 4. 5. 6.

Thread 0 termin : 00:00:01.0156250 Thread 1 termin : Thread 2 termin : Thread 3 termin : Thread 4 termin : Fin du thread Main

dbut 11:18:50, dure programme 1 s, fin 11:18:51, dure effective dbut 11:18:50, dbut 11:18:50, dbut 11:18:50, dbut 11:18:50, 11:18:55 dure dure dure dure programme programme programme programme 2 3 4 5 s, s, s, s, fin fin fin fin 11:18:52, 11:18:53, 11:18:54, 11:18:55, dure dure dure dure effective effective effective effective 00:00:02 00:00:03 00:00:04 00:00:05

Cet exemple montre que deux threads peuvent s'changer de l'information : le thread lanceur peut contrler l'excution du thread lanc en lui donnant des informations le thread lanc peut rendre des rsultats au thread lanceur. Pour que le thread lanc sache quel moment les rsultats qu'il attend sont disponibles, il faut qu'il soit averti de la fin du thread lanc. Ici, il a attendu qu'il se termine en utilisant la mthode Join. Il y a d'autres faons de faire la mme chose. Nous les verrons ultrieurement.

8.5
8.5.1

Accs concurrents des ressources partages


Accs concurrents non synchroniss

Dans le paragraphe sur l'change d'informations entre threads, l'information change ne l'tait que par deux threads et des moments bien prcis. On avait l un classique passage de paramtres. Il existe d'autres cas o une information est partage par plusieurs threads qui peuvent vouloir la lire ou la mettre jour au mme moment. Se pose alors le problme de l'intgrit de cette information. Supposons que l'information partage soit une structure S avec diverses informations I1, I2, ... In.

un thread T1 commence mettre jour la structure S : il modifie le champ I1 et est interrompu avant d'avoir termin la mise jour complte de la structure S un thread T2 qui rcupre le processeur lit alors la structure S pour prendre des dcisions. Il lit une structure dans un tat instable : certains champs sont jour, d'autres pas.

On appelle cette situation, l'accs une ressource partage, ici la structure S, et elle est souvent assez dlicate grer. Prenons l'exemple suivant pour illustrer les problmes qui peuvent surgir : une application va gnrer n threads, n tant pass en paramtre la ressource partage est un compteur qui devra tre incrment par chaque thread gnr la fin de l'application, la valeur du compteur est affiche. On devrait donc trouver n. Le programme est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. using System; using System.Threading; namespace Chap8 { class Program { // variables de classe static int cptrThreads = 0; // compteur de threads

//main public static void Main(string[] args) { // mode d'emploi const string syntaxe = "pg nbThreads"; const int nbMaxThreads = 100; // vrification nbre d'arguments if (args.Length != 1) { // erreur Console.WriteLine(syntaxe); // arrt Environment.Exit(1); } // vrification qualit de l'argument int nbThreads = 0; bool erreur = false; try { nbThreads = int.Parse(args[0]); if (nbThreads < 1 || nbThreads > nbMaxThreads) erreur = true; } catch { // erreur

Les threads d'excution

278

32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73.

erreur = true; } // erreur ? if (erreur) { // erreur Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)"); // fin Environment.Exit(2); } // cration et gnration des threads Thread[] threads = new Thread[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration threads[i] = new Thread(Incrmente); // nommage threads[i].Name = "" + i; // lancement threads[i].Start(); }//for // attente de la fin des threads for (int i = 0; i < nbThreads; i++) { threads[i].Join(); } // affichage compteur Console.WriteLine("Nombre de threads gnrs : " + cptrThreads); } public static void Incrmente() { // augmente le compteur de threads // lecture compteur int valeur = cptrThreads; // suivi Console.WriteLine("A {0:hh:mm:ss}, le thread {1} DateTime.Now, Thread.CurrentThread.Name, cptrThreads); // attente Thread.Sleep(1000); // incrmentation compteur cptrThreads = valeur + 1; // suivi Console.WriteLine("A {0:hh:mm:ss}, le thread {1} DateTime.Now, Thread.CurrentThread.Name, cptrThreads); } } }

a lu la valeur du compteur : {2}",

a crit la valeur du compteur : {2}",

Nous ne nous attarderons pas sur la partie gnration de threads dj tudie. Intressons-nous plutt la mthode Incrmente, de la ligne 59 utilise par chaque thread pour incrmenter le compteur statique cptrThreads de la ligne 8. 1. ligne 62 : le compteur est lu 2. ligne 66 : le thread s'arrte 1 s. Il perd donc le processeur 3. ligne 68 : le compteur est incrment L'tape 2 n'est l que pour forcer le thread perdre le processeur. Celui-ci va tre donn un autre thread. Dans la pratique, rien n'assure qu'un thread ne sera pas interrompu entre le moment o il va lire le compteur et le moment o il va l'incrmenter. Mme si on crit cptrThreads++, donnant ainsi l'illusion d'une instruction unique, le risque existe de perdre le processeur entre le moment o on lit la valeur du compteur et celui on crit sa valeur incrmente de 1. En effet, l'opration de haut niveau cptrThreads++ va faire l'objet de plusieurs instructions lmentaires au niveau du processeur. L'tape 2 de sommeil d'une seconde n'est donc l que pour systmatiser ce risque. Les rsultats obtenus avec 5 threads sont les suivants :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. A 12:00:56, le thread 3 a lu la A 12:00:56, le thread 2 a lu la A 12:00:56, le thread 1 a lu la A 12:00:56, le thread 0 a lu la A 12:00:56, le thread 4 a lu la A 12:00:57, le thread 3 a crit A 12:00:57, le thread 2 a crit A 12:00:57, le thread 1 a crit A 12:00:57, le thread 0 a crit A 12:00:57, le thread 4 a crit Nombre de threads gnrs : 1 valeur du valeur du valeur du valeur du valeur du la valeur la valeur la valeur la valeur la valeur compteur : 0 compteur : 0 compteur : 0 compteur : 0 compteur : 0 du compteur : du compteur : du compteur : du compteur : du compteur :

1 1 1 1 1

A la lecture de ces rsultats, on voit bien ce qui se passe :

Les threads d'excution

279

ligne 1 : un premier thread lit le compteur. Il trouve 0. Il s'arrte 1 s donc perd le processeur ligne 2 : un second thread prend alors le processeur et lit lui aussi la valeur du compteur. Elle est toujours 0 puisque le thread prcdent ne l'a pas encore incrmente. Il s'arrte lui aussi 1 s et perd son tour le processeur. lignes 1-5 : en 1 s, les 5 threads ont le temps de passer tous et de lire tous la valeur 0. lignes 6-10 : lorsqu'ils vont se rveiller les uns aprs les autres, ils vont incrmenter la valeur 0 qu'ils ont lue et crire la valeur 1 dans le compteur, ce que confirme le programme principal (Main) en ligne 11.

D'o vient le problme ? Le second thread a lu une mauvaise valeur du fait que le premier avait t interrompu avant d'avoir termin son travail qui tait de mettre jour le compteur dans la fentre. Cela nous amne la notion de ressource critique et de section critique d'un programme: une ressource critique est une ressource qui ne peut tre dtenue que par un thread la fois. Ici la ressource critique est le compteur. une section critique d'un programme est une squence d'instructions dans le flux d'excution d'un thread au cours de laquelle il accde une ressource critique. On doit assurer qu'au cours de cette section critique, il est le seul avoir accs la ressource. Dans notre exemple, la section critique est le code situ entre la lecture du compteur et l'criture de sa nouvelle valeur :
1. 2. 3. 4. 5. 6. // lecture compteur int valeur = cptrThreads; // attente Thread.Sleep(1000); // incrmentation compteur cptrThreads = valeur + 1;

Pour excuter ce code, un thread doit tre assur d'tre tout seul. Il peut tre interrompu mais pendant cette interruption, un autre thread ne doit pas pouvoir excuter ce mme code. La plate-forme .NET offre divers outils pour assurer l'entre unitaire dans les sections critiques de code. Nous en voyons quelques-uns maintenant.

8.5.2

La clause lock

La clause lock permet de dlimiter une section critique de la faon suivante :


lock(obj){section critique}

obj doit tre une rfrence d'objet visible par tous les threads excutant la section critique. La clause lock assure qu'un seul thread la fois excutera la section critique. L'exemple prcdent est rcrit comme suit :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. using System; using System.Threading; namespace Chap8 { class Program2 { // variables de classe static int cptrThreads = 0; // compteur de threads static object synchro = new object(); // objet de synchronisation //main public static void Main(string[] args) { ... // attente de la fin des threads Thread.CurrentThread.Name = "Main"; for (int i = nbThreads - 1; i >= 0; i--) { Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name); threads[i].Join(); Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a t prvenu de la fin du thread {2}", DateTime.Now, Thread.CurrentThread.Name, threads[i].Name); } // affichage compteur Console.WriteLine("Nombre de threads gnrs : " + cptrThreads); }

public static void Incrmente() { // augmente le compteur de threads // un accs exclusif au compteur est demand Console.WriteLine("A {0:hh:mm:ss}, le thread {1} attend l'autorisation d'entrer dans la section critique", DateTime.Now, Thread.CurrentThread.Name); 29. lock (synchro) {

Les threads d'excution

280

30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44.

// lecture compteur int valeur = cptrThreads; // suivi Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a lu la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads); // attente Thread.Sleep(1000); // incrmentation compteur cptrThreads = valeur + 1; // suivi Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a crit la valeur du compteur : {2}", DateTime.Now, Thread.CurrentThread.Name, cptrThreads); } Console.WriteLine("A {0:hh:mm:ss}, le thread {1} a quitt la section critique", DateTime.Now, Thread.CurrentThread.Name); } } }

ligne 9 : synchro est l'objet qui va permettre la synchronisation de tous les threads. lignes 16-23 : la mthode [Main] attend les threads dans l'ordre inverse de leur cration. lignes 29-40 : la section critique de la mthode Incrmente a t encadre par la clause lock.

Les rsultats obtenus avec 3 threads sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. A 09:37:09, le thread 0 attend l'autorisation d'entrer dans la section critique A 09:37:09, le thread 0 a lu la valeur du compteur : 0 A 09:37:09, le thread 1 attend l'autorisation d'entrer dans la section critique A 09:37:09, le thread 2 attend l'autorisation d'entrer dans la section critique A 09:37:09, le thread Main attend la fin du thread 2 A 09:37:10, le thread 0 a crit la valeur du compteur : 1 A 09:37:10, le thread 1 a lu la valeur du compteur : 1 A 09:37:10, le thread 0 a quitt la section critique A 09:37:11, le thread 1 a crit la valeur du compteur : 2 A 09:37:11, le thread 1 a quitt la section critique A 09:37:11, le thread 2 a lu la valeur du compteur : 2 A 09:37:12, le thread 2 a crit la valeur du compteur : 3 A 09:37:12, le thread 2 a quitt la section critique A 09:37:12, le thread Main a t prvenu de la fin du thread 2 A 09:37:12, le thread Main attend la fin du thread 1 A 09:37:12, le thread Main a t prvenu de la fin du thread 1 A 09:37:12, le thread Main attend la fin du thread 0 A 09:37:12, le thread Main a t prvenu de la fin du thread 0 Nombre de threads gnrs : 3

le thread 0 entre le 1er dans la section critique : lignes 1, 2, 6, 8 les deux autres threads vont tre bloqus tant que le thread 0 ne sera pas sorti de la section critique : lignes 3 et 4 le thread 1 passe ensuite : lignes 7, 9, 10 le thread 2 passe ensuite : lignes 11, 12, 13 ligne 14 : le thread Main qui attendait la fin du thread 2 est prvenu ligne 15 : le thread Main attend maintenant la fin du thread 1. Celui-ci est dj termin. Le thread Main en est prvenu immdiatement, ligne 16. lignes 17-18 : le mme processus se passe avec le thread 0 ligne 19 : le nombre de threads est correct

8.5.3

La classe Mutex

La classe System.Threading.Mutex permet elle aussi de dlimiter des sections critiques. Elle diffre de la clause lock en terme de visibilit : la clause lock permet de synchroniser des threads d'une mme application la classe Mutex permet de synchroniser des threads de diffrentes applications. Nous utiliserons le constructeur et les mthodes suivants :
public Mutex() public bool WaitOne()

cre un Mutex M Le thread T1 qui excute l'opration M.WaitOne() demande la proprit de l'objet de synchronisation M. Si le Mutex M n'est dtenu par aucun thread (le cas au dpart), il est "donn" au thread T1 qui l'a demand. Si un peu plus tard, un thread T2 fait la mme opration, il sera bloqu. En effet, un Mutex ne peut appartenir qu' un thread. Il sera dbloqu lorsque le thread T1 librera

Les threads d'excution

281

public void ReleaseMutex()

le Mutex M qu'il dtient. Plusieurs threads peuvent ainsi tre bloqus en attente du Mutex M. Le thread T1 qui effectue l'opration M.ReleaseMutex() abandonne la proprit du Mutex M. Lorsque le thread T1 perdra le processeur, le systme pourra donner celui-ci l'un des threads en attente du Mutex M. Un seul l'obtiendra son tour, les autres en attente de M restant bloqus

Un Mutex M gre l'accs une ressource partage R. Un thread demande la ressource R par M.WaitOne() et la rend par M.ReleaseMutex(). Une section critique de code qui ne doit tre excute que par un seul thread la fois est une ressource partage. La synchronisation d'excution de la section critique peut se faire ainsi :
M.WaitOne(); // le thread est seul entrer ici // section critique .... M.ReleaseMutex();

o M est un objet Mutex. Il ne faut pas oublier de librer un Mutex devenu inutile afin qu'un autre thread puisse entrer dans la section critique, sinon les threads en attente du Mutex jamais libr n'auront jamais accs au processeur. Si nous mettons en pratique sur l'exemple prcdent ce que nous venons de voir, notre application devient la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. using System; using System.Threading; namespace Chap8 { class Program3 { // variables de classe static int cptrThreads = 0; // compteur de threads static Mutex synchro = new Mutex(); // objet de synchronisation //main public static void Main(string[] args) { ... } public static void Incrmente() { .... synchro.WaitOne(); try { ... } finally { ... synchro.ReleaseMutex(); } } } }

ligne 9 : l'objet de synchronisation des threads est dsormais un Mutex. ligne 18 : dbut de la section critique - un seul thread doit y entrer. On se bloque jusqu' ce que le Mutex synchro soit libre. ligne 33 : parce qu'un Mutex doit toujours tre libr, exception ou pas, on gre la section critique avec un try / finally afin de librer le Mutex dans le finally. ligne 23 : le Mutex est libr une fois la section critique passe.

Les rsultats obtenus sont les mmes que prcdemment.

8.5.4

La classe AutoResetEvent

Un objet AutoResetEvent est une barrire ne laissant passer qu'un thread la fois, comme les deux outils prcdents lock et Mutex. On construit un objet AutoResetEvent de la faon suivante :
AutoResetEvent barrire=new AutoresetEvent(bool tat);

Le boolen tat indique l'tat ferm (false) ou ouvert (true) de la barrire. Un thread voulant passer la barrire l'indiquera de la faon suivante :
barrire.WaitOne();

Les threads d'excution

282

si la barrire est ouverte, le thread passe et la barrire est referme derrire lui. Si plusieurs threads attendaient, on est assur qu'un seul passera. si la barrire est ferme, le thread est bloqu. Un autre thread l'ouvrira lorsque le moment sera venu. Ce moment est entirement dpendant du problme trait. La barrire sera ouverte par l'opration :

barrire.Set();

Il peut arriver qu'un thread veuille fermer une barrire. Il pourra le faire par :
barrire.Reset();

Si dans l'exemple prcdent, on remplace l'objet Mutex par un objet de type AutoResetEvent, le code devient le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. using System; using System.Threading; namespace Chap8 { class Program4 { // variables de classe static int cptrThreads = 0; // compteur de threads static EventWaitHandle synchro = new AutoResetEvent(false); // objet de synchronisation //main public static void Main(string[] args) { .... // on ouvre la barrire de la section critique Console.WriteLine("A {0:hh:mm:ss}, le thread {1} ouvre la barrire de la section critique", DateTime.Now, Thread.CurrentThread.Name); synchro.Set(); // attente de la fin des threads ... // affichage compteur Console.WriteLine("Nombre de threads gnrs : " + cptrThreads); } public static void Incrmente() { // augmente le compteur de threads // un accs exclusif au compteur est demand ... ... ... } } } } synchro.WaitOne(); try { } finally { // on relche la ressource synchro.Set();

ligne 9 : la barrire est cre ferme. Elle sera ouverte par le thread Main ligne 16. ligne 27 : le thread charg d'incrmenter le compteur de threads demande l'autorisation d'entrer dans la section critique. Les diffrents threads vont s'accumuler devant la barrire ferme. Lorque le thread Main va l'ouvrir, l'un des threads en attente va passer. ligne 33 : lorsqu'il a termin son travail, il rouvre la barrire permettant un autre thread d'entrer.

On obtient des rsultats analogues aux prcdents.

8.5.5

La classe Interlocked

La classe Interlocked permet de rendre atomique un groupe d'oprations. Dans un groupe d'oprations atomique, soit toutes les oprations sont excutes par le thread qui excute le groupe soit aucune. On ne reste pas dans un tat ou certaines ont t excutes et d'autres pas. Les objets de synchronisation lock, Mutex, AutoResetEvent ont toutes pour but de rendre atomique un groupe d'oprations. Ce rsultat est obtenu au prix du blocage de threads. La classe Interlocked permet, pour des oprations simples mais assez frquentes, d'viter le blocage de threads. La classe Interlocked offre les mthodes statiques suivantes :

Les threads d'excution

283

La mthode Increment a la signature suivante :


public static int Increment(ref int location);

Elle permet d'incrmenter de 1 le paramtre location. L'opration est garantie atomique. Notre programme de comptage de threads peut alors tre le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. using System; using System.Threading; namespace Chap8 { class Program5 { // variables de classe static int cptrThreads = 0; // compteur de threads

...

//main public static void Main(string[] args) { } public static void Incrmente() { // incrmente le compteur de threads Interlocked.Increment(ref cptrThreads); }

} }

ligne 17 : le compteur de threads est incrment de faon atomique.

8.6
8.6.1

Accs concurrents des ressources partages multiples


Un exemple

Dans nos exemples prcdents, une unique ressource tait partage par les diffrents threads. La situation peut se compliquer s'il y en a plusieurs et qu'elles sont dpendantes les unes des autres. Une situation d'interblocage peut notamment survenir. Cette situation appele galement deadlock est celle dans laquelle deux threads s'attendent mutuellement. Considrons les actions suivantes qui se suivent dans le temps : un thread T1 obtient la proprit d'un Mutex M1 pour avoir accs une ressource partage R1 un thread T2 obtient la proprit d'un Mutex M2 pour avoir accs une ressource partage R2 le thread T1 demande le Mutex M2. Il est bloqu. le thread T2 demande le Mutex M1. Il est bloqu.

Ici, les threads T1 et T2 s'attendent mutuellement. Ce cas apparat lorsque des threads ont besoin de deux ressources partages, la ressource R1 contrle par le Mutex M1 et la ressource R2 contrle par le Mutex M2. Une solution possible est de demander les deux ressources en mme temps l'aide d'un Mutex unique M. Mais ce n'est pas toujours possible si par exemple cela entrane une mobilisation longue d'une ressource coteuse. Une autre solution est qu'un thread ayant M1 et ne pouvant obtenir M2, relche alors M1 pour viter l'interblocage. Considrons l'exemple suivant : 1. 2. On a un tableau dans lequel des threads viennent dposer des donnes (les crivains) et d'autres viennent les lire (les lecteurs). Les crivains sont gaux entre-eux mais exclusifs : un seul crivain la fois peut dposer ses donnes dans le tableau.

Les threads d'excution

284

3. 4.

Les lecteurs sont gaux entre-eux mais exclusifs : un seul lecteur la fois peut lire les donnes dposes dans le tableau. Un lecteur ne peut lire les donnes du tableau que lorsqu'un crivain en a dpos dedans et un crivain ne peut dposer de nouvelles donnes dans le tableau que lorsque celles qui y sont ont t lues par un lecteur.

On peut distinguer deux ressources partages : le tableau en criture : un seul crivain la fois doit y avoir accs. le tableau en lecture : un seul lecteur la fois doit y avoir accs. et un ordre d'utilisation de ces ressources : un lecteur doit toujours passer aprs un crivain. un crivain doit toujours passer aprs un lecteur, sauf la 1re fois. On peut contrler l'accs ces deux ressources avec deux barrires de type AutoResetEvent :

la barrire peutEcrire contrlera l'accs des crivains au tableau. la barrire peutLire contrlera l'accs des lecteurs au tableau. la barrire peutEcrire sera cre initialement ouverte laissant passer ainsi un 1er crivain et bloquant tous les autres. la barrire peutLire sera cre initialement ferme bloquant tous les lecteurs. lorsqu'un crivain aura termin son travail, il ouvrira la barrire peutLire pour laisser entrer un lecteur. lorsqu'un lecteur aura termin son travail, il ouvrira la barrire peutEcrire pour laisser entrer un crivain.

Le programme illustrant cette synchronisation par vnements est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. using System; using System.Threading; namespace Chap8 { class Program { // utilisation de threads lecteurs et crivains // illustre l'utilisation d'vnements de synchronisation // variables de classe static int[] data = new int[3]; // ressource partage entre threads lecteur et threads crivain static Random objRandom = new Random(DateTime.Now.Second); // un gnrateur de nombres alatoires static AutoResetEvent peutLire; // signale qu'on peut lire le contenu de data static AutoResetEvent peutEcrire; // signale qu'on peut crire le contenu de data //main public static void Main(string[] args) { // le nbre de threads gnrer const int nbThreads = 2; // initialisation des drapeaux peutLire = new AutoResetEvent(false); // on ne peut pas encore lire peutEcrire = new AutoResetEvent(true); // on peut dj crire // cration des threads lecteurs Thread[] lecteurs = new Thread[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration lecteurs[i] = new Thread(Lire); lecteurs[i].Name = "L" + i.ToString(); // lancement lecteurs[i].Start(); } // cration des threads crivains Thread[] crivains = new Thread[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration crivains[i] = new Thread(Ecrire); crivains[i].Name = "E" + i.ToString(); // lancement crivains[i].Start(); } //fin de main Console.WriteLine("Fin de Main..."); } // lire le contenu du tableau public static void Lire() { ... } // crire dans le tableau public static void Ecrire() { }

....

Les threads d'excution

285

59. } 60. }

ligne 11 : le tableau data est la ressource partage entre les threads lecteurs et crivains. Elle est partage en lecture par les threads lecteurs, en criture par les threads crivains. ligne 13 : l'objet peutLire sert avertir les threads lecteurs qu'ils peuvent lire le tableau data. Il est mis vrai par le thread crivain ayant rempli le tableau data. Il est initialis false, ligne 23. Il faut qu'un thread crivain remplisse d'abord le tableau avant de passer l'vnement peutLire vrai. ligne 14 : l'objet peutEcrire sert avertir les threads crivains qu'ils peuvent crire dans le tableau data. Il est mis vrai par le thread lecteur ayant exploit la totalit du tableau data. Il est initialis true, ligne 24. En effet, le tableau data est libre en criture. lignes 27-34 : cration et lancement des threads lecteurs lignes 37-44 : cration et lancement des threads crivains

La mthode Lire excute par les threads lecteurs est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. public static void Lire() { // suivi Console.WriteLine("Mthode [Lire] dmarre par le thread n {0}", Thread.CurrentThread.Name); // on doit attendre l'autorisation de lecture peutLire.WaitOne(); // lecture tableau for (int i = 0; i < data.Length; i++) { //attente 1 s Thread.Sleep(1000); // affichage Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]); } // on peut crire peutEcrire.Set(); // suivi Console.WriteLine("Mthode [Lire] termine par le thread n {0}", Thread.CurrentThread.Name); }

ligne 5 : on attend qu'un thread crivain signale que le tableau a t rempli. Lorsque ce signal sera reu, un seul des threads lecteurs en attente de ce signal pourra passer. lignes 7-12 : exploitation du tableau data avec un Sleep au milieu pour forcer le thread perdre le processeur. ligne 14 : indique aux threads crivains que le tableau a t lu et qu'il peut tre rempli de nouveau.

La mthode Ecrire excute par les threads crivains est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. public static void Ecrire() { // suivi Console.WriteLine("Mthode [Ecrire] dmarre par le thread n {0}", Thread.CurrentThread.Name); // on doit attendre l'autorisation d'criture peutEcrire.WaitOne(); // criture tableau for (int i = 0; i < data.Length; i++) { //attente 1 s Thread.Sleep(1000); // affichage data[i] = objRandom.Next(0, 1000); Console.WriteLine("{0:hh:mm:ss} : L'crivain {1} a crit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]); } // on peut lire peutLire.Set(); // suivi Console.WriteLine("Mthode [Ecrire] termine par le thread n {0}", Thread.CurrentThread.Name); }

ligne 5 : on attend qu'un thread lecteur signale que le tableau a t lu. Lorsque ce signal sera reu, un seul des threads crivains en attente de ce signal pourra passer. lignes 7-13 : exploitation du tableau data avec un Sleep au milieu pour forcer le thread perdre le processeur. ligne 15 : indique aux threads lecteurs que le tableau a t rempli et qu'il peut tre lu de nouveau.

Les threads d'excution

286

L'excution donne les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. Mthode [Lire] dmarre par le thread n L0 Mthode [Lire] dmarre par le thread n L1 Mthode [Ecrire] dmarre par le thread n E0 Mthode [Ecrire] dmarre par le thread n E1 Fin de Main... 02:29:18 : L'crivain E0 a crit le nombre 607 02:29:19 : L'crivain E0 a crit le nombre 805 02:29:20 : L'crivain E0 a crit le nombre 650 Mthode [Ecrire] termine par le thread n E0 02:29:21 : Le lecteur L0 a lu le nombre 607 02:29:22 : Le lecteur L0 a lu le nombre 805 02:29:23 : Le lecteur L0 a lu le nombre 650 Mthode [Lire] termine par le thread n L0 02:29:24 : L'crivain E1 a crit le nombre 186 02:29:25 : L'crivain E1 a crit le nombre 881 02:29:26 : L'crivain E1 a crit le nombre 415 Mthode [Ecrire] termine par le thread n E1 02:29:27 : Le lecteur L1 a lu le nombre 186 02:29:28 : Le lecteur L1 a lu le nombre 881 02:29:29 : Le lecteur L1 a lu le nombre 415 Mthode [Lire] termine par le thread n L1

On peut remarquer les points suivants :


on a bien 1 seul lecteur la fois, bien que celui-ci perde le processeur dans la section critique Lire on a bien 1 seul crivain la fois, bien que celui-ci perde le processeur dans la section critique Ecrire un lecteur ne lit que lorsqu'il y a quelque chose lire dans le tableau un crivain n'crit que lorsque le tableau a t entirement lu

8.6.2

La classe Monitor

Dans l'exemple prcdent : il y a deux ressources partages grer pour une ressource donne, les threads sont gaux. Lorsque les threads crivains sont bloqus sur l'instruction peutEcrire.WaitOne, l'un d'entre-eux, n'importe lequel, est dbloqu par l'opration peutEcrire.Set. Si l'opration prcdente doit ouvrir la barrire un crivain en particulier, les choses deviennent plus compliques. On peut considrer l'analogie avec un tablissement accueillant du public des guichets o chaque guichet est spcialis. Lorsque le client arrive, il prend un ticket au distributeur de tickets pour le guichet X puis va s'asseoir. Chaque ticket est numrot et les clients sont appels par leur numro via un haut-parleur. Pendant son attente, le client fait ce qu'il veut. Il peut lire ou somnoler. Il est rveill chaque fois par le haut-parleur qui annonce que le n Y est appel au guichet X. S'il s'agit de lui, le client se lve et accde au guichet X, sinon il continue ce qu'il faisait. On peut ici fonctionner de faon analogue. Prenons l'exemple des crivains : plusieurs crivains attendent pour un mme guichet le guichet se libre et le n de l'crivain suivant est appel leurs threads sont bloqus le thread qui utilisait le tableau en lecture indique aux crivains que le tableau est disponible. Lui ou un autre thread a fix le thread crivain qui doit passer la barrire. chaque thread vrifie s'il est l'lu. Si oui, il passe la barrire. Si non, il se remet en attente.

chaque crivain regarde son n et seul celui qui a le n appel va au guichet. Les autres se remettent en attente. La classe Monitor permet de mettre en oeuvre ce scnario.

Les threads d'excution

287

Nous dcrivons maintenant une construction standard (pattern), propose dans le chapitre Threading du livre C# 3.0 rfrenc dans l'introduction de ce document, capable de rsoudre les problmes de barrire avec condition d'entre.

Tout d'abord, les threads qui se partagent une ressource (le guichet, ...) y accdent via un objet que nous appellerons un jeton. Pour ouvrir la barrire qui mne au guichet, il faut avoir le jeton pour l'ouvrir et il n'y a qu'un seul jeton. Les threads doivent donc se passer le jeton entre-eux.
object jeton=new object();

Pour aller au guichet, les threads demandent tout d'abord le jeton :


Monitor.Enter(jeton);

Si le jeton est libre, il est donn au thread ayant excut l'opration prcdente, sinon le thread est mis en attente du jeton.

Si l'accs au guichet se fait de faon non ordonne, c.a.d. dans le cas o la personne qui entre n'importe pas, l'opration prcdente est suffisante. Le thread ayant le jeton va au guichet. Si l'accs se fait de faon ordonne, le thread qui a le jeton vrifie qu'il remplit la condition pour aller au guichet :
while (! jeNeSuisPasCeluiQuiEstAttendu) {Monitor.Wait(jeton);}

Si le thread n'est pas celui qui est attendu au guichet, il laisse son tour en redonnant le jeton. Il passe dans un tat bloqu. Il sera rveill ds que le jeton redeviendra disponible pour lui. Il vrifiera alors de nouveau s'il vrifie la condition pour aller au guichet. L'opration Monitor.Wait(jeton) qui relche le jeton ne peut tre faite que si le thread est propritaire du jeton. Si ce n'est pas le cas, une exception est lance.

1. 2.

Le thread qui vrifie la condition pour aller au guichet y va :


// travail au guichet ....

Avant de quitter le guichet, le thread doit rendre son jeton, sinon les threads bloqus en attente de celui-ci le resteront indfiniment. Il y a deux situations diffrentes :

la premire situation est celle o le thread ayant le jeton est galement celui qui signale aux threads en attente du jeton que celui-ci est libre. Il le fera de la faon suivante :
1. 2. 3. 4. 5. 6. 7. 8. // travail au guichet .... // modification condition d'accs au guichet ... // rveil des threads en attente du jeton Monitor.PulseAll(jeton); // libration du jeton Monitor.Exit(jeton);

Ligne 6, il rveille les threads en attente du jeton. Ce rveil signifie qu'ils deviennent ligibles pour recevoir le jeton. Cela ne veut pas dire qu'ils le reoivent immdiatement. Ligne 8, le jeton est libr. Tous les threads ligibles vont recevoir tour tour le jeton, de faon indterministe. Cela va leur donner l'occasion de vrifier de nouveau s'ils vrifient la condition d'accs. Le thread ayant libr le jeton a modifi cette condition ligne 4 afin de permettre un nouveau thread d'entrer. Le premier qui la vrifie garde le jeton et va au guichet son tour.

la seconde situation est celle o le thread ayant le jeton n'est pas celui qui doit signaler aux threads en attente du jeton que celui-ci est libre. Il doit nanmoins le librer parce que le thread charg d'envoyer ce signal doit tre dtenteur du jeton. Il le fera par l'opration :

Les threads d'excution

288

Monitor.Exit(jeton);

Le jeton est dsormais disponible, mais les threads qui l'attendent (ils ont fait une opration Wait(jeton)) n'en sont pas avertis. Cette tche est confie un autre thread qui un moment donn excutera un code similaire au suivant :
1. 2. 3. 4. 5. 6. 7. 8. // acquisition jeton Monitor.Enter(jeton); // modification condition d'accs au guichet .... // rveil des threads en attente du jeton Monitor.PulseAll(jeton); // libration du jeton Monitor.Exit(jeton);

Au final, la construction standard propose dans le chapitre Threading du livre C# 3.0 est la suivante :

dfinir le jeton d'accs au guichet :


object jeton=new object();

demander l'accs au guichet :


lock(jeton){ while (! jeNeSuisPasCeluiQuiEstAttendu) Monitor.Wait(jeton); } // passage au guichet ... lock(jeton){...}

est quivalent
Monitor.Enter(jeton); try{...} finally{Monitor.Exit(jeton);}

On notera que dans ce schma le jeton est relch immdiatement, ds que la barrire est passe. Un autre thread peut alors tester la condition d'accs. La construction prcdente laisse donc entrer tous les threads vrifiant la condition d'accs. Si ce n'est pas ce qui est dsir, on pourra crire :
lock(jeton){ while (! jeNeSuisPasCeluiQuiEstAttendu) Monitor.Wait(jeton); // passage au guichet ... }

o le jeton n'est relch qu'aprs le passage au guichet.

modifier la condition d'accs au guichet et en avertir les autres threads


lock(jeton){ // modifier la condition d'accs au guichet ... // en avertir les threads en attente du jeton Monitor.PulseAll(jeton); }

Ci-dessus, la condition d'accs ne peut tre modifie que par le thread ayant le jeton. On pourra aussi crire :
// modifier la condition d'accs au guichet ... // en avertir les threads en attente du jeton Monitor.PulseAll(jeton); // librer le jeton Monitor.Exit(jeton);

si le thread a dj le jeton.

Les threads d'excution

289

Muni de ces informations, nous pouvons rcrire l'application lecteurs / crivains en fixant un ordre des lecteurs et des crivains pour l'accs leurs guichets respectifs. Le code est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. using System; using System.Threading; namespace Chap8 { class Program2 { // utilisation de threads lecteurs et crivains // illustre l'utilisation d'vnements de synchronisation // variables de classe static int[] data = new int[3]; // ressource partage entre threads lecteur et threads crivain static Random objRandom = new Random(DateTime.Now.Second); // un gnrateur de nombres alatoires static object peutLire = new object(); // signale qu'on peut lire le contenu de data static object peutEcrire = new object(); // signale qu'on peut crire le contenu de data static bool lectureAutorise = false; // pour autoriser la lecture du tableau static bool critureAutorise = false; // pour autoriser l'criture dans le tableau static string[] ordreLecture; // fixe l'ordre des lecteurs static string[] ordreEcriture; // fixe l'ordre des crivains static int lecteurSuivant = 0; // indique le n du lecteur suivant static int crivainSuivant = 0; // indique le n de l'crivain suivant //main public static void Main(string[] args) { // le nbre de threads gnrer const int nbThreads = 5; // cration des threads lecteurs Thread[] lecteurs = new Thread[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration lecteurs[i] = new Thread(Lire); lecteurs[i].Name = "L" + i.ToString(); // lancement lecteurs[i].Start(); } // cration de l'ordre de lecture ordreLecture = new string[nbThreads]; for (int i = 0; i < nbThreads; i++) { ordreLecture[i] = lecteurs[nbThreads - i - 1].Name; Console.WriteLine("Le lecteur {0} est en position {1}", ordreLecture[i], i); } // cration des threads crivains Thread[] crivains = new Thread[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration crivains[i] = new Thread(Ecrire); crivains[i].Name = "E" + i.ToString(); // lancement crivains[i].Start(); } // cration de l'ordre d'criture ordreEcriture = new string[nbThreads]; for (int i = 0; i < nbThreads; i++) { ordreEcriture[i] = crivains[i].Name; Console.WriteLine("L'crivain {0} est en position {1}", ordreEcriture[i], i); } // autorisation d'criture lock (peutEcrire) { critureAutorise = true; Monitor.Pulse(peutEcrire); } //fin de main Console.WriteLine("Fin de Main..."); } // lire le contenu du tableau

Les threads d'excution

290

74. 75. 76. 77. 78. 79. 80. 81. 82. 83.

public static void Lire() { ... } // crire dans le tableau public static void Ecrire() { }

... } }

L'accs au guichet de lecture est conditionn par les lments suivants :


ligne 13 : le jeton peutLire ligne 15 : le boolen lectureAutorise ligne 17 : le tableau ordonn des lecteurs. Les lecteurs vont au guichet de lecture dans l'ordre de ce tableau qui contient leurs noms. ligne 19 : lecteurSuivant indique le n du prochain lecteur autoris aller au guichet.

L'accs au guichet d'criture est conditionn par les lments suivants :


ligne 14 : le jeton peutEcrire ligne 16 : le boolen critureAutorise ligne 18 : le tableau ordonn des crivains. Les crivains vont au guichet d'criture dans l'ordre de ce tableau qui contient leurs noms. ligne 20 : crivainSuivant indique le n du prochain crivain autoris aller au guichet.

Les autres lments du code sont les suivants :


lignes 29-36 : cration et lancement des threads lecteurs. Ils seront tous bloqus car la lecture n'est pas autorise (ligne 15). lignes 39-43 : leur ordre de passage au guichet se fera dans l'ordre inverse de leur cration. lignes 46-53 : cration et lancement des threads rivains. Ils seront tous bloqus car l'criture n'est pas autorise (ligne 16). lignes 56-60 : leur ordre de passage au guichet se fera dans l'ordre de leur cration. ligne 64 : on autorise l'criture ligne 65 : on avertit les crivains que quelque chose a chang.

La mthode Lire est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. public static void Lire() { // suivi Console.WriteLine("Mthode [Lire] dmarre par le thread n {0}", Thread.CurrentThread.Name); // on doit attendre l'autorisation de lecture lock (peutLire) { while (!lectureAutorise || ordreLecture[lecteurSuivant] != Thread.CurrentThread.Name) { Monitor.Wait(peutLire); } // lecture tableau for (int i = 0; i < data.Length; i++) { //attente 1 s Thread.Sleep(1000); // affichage Console.WriteLine("{0:hh:mm:ss} : Le lecteur {1} a lu le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]); } // lecteur suivant lectureAutorise = false; lecteurSuivant++; // on prvient les crivains qu'ils peuvent crire lock (peutEcrire) { critureAutorise = true; Monitor.PulseAll(peutEcrire); }

// suivi Console.WriteLine("Mthode [Lire] termine par le thread n {0}", Thread.CurrentThread.Name); 27. } 28. }

Les threads d'excution

291

l'ensemble de l'accs au guichet est contrl par le lock des lignes 5-27. Le lecteur qui rcupre le jeton le garde pendant tout son passage au guichet lignes 6-8 : un lecteur ayant acquis le jeton ligne 5 le relche si la lecture n'est pas autorise ou si ce n'est pas son tour de passer. lignes 10-15 : passage au guichet (exploitation du tableau) lignes 17-18 : le thread change les conditions d'accs au guichet de lecture. On notera qu'il a toujours le jeton de lecture et que ces modifications ne peuvent pas encore permettre un lecteur de passer. lignes 20-23 : le thread change les conditions d'accs au guichet d'criture et prvient tous les crivains en attente que quelque chose a chang. ligne 27 : le lock se termine, le jeton peutLire est relch. Un thread de lecture pourrait alors l'acqurir ligne 5 mais il ne passerait pas la condition d'accs puisque le boolen lectureAutorise est faux. Par ailleurs, tous les threads qui sont en attente du jeton peutLire le restent car l'opration PulseAll(peutLire) n'a pas encore eu lieu.

La mthode Ecrire est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. public static void Ecrire() { // suivi Console.WriteLine("Mthode [Ecrire] dmarre par le thread n {0}", Thread.CurrentThread.Name); // on doit attendre l'autorisation d'criture lock (peutEcrire) { while (!critureAutorise || ordreEcriture[crivainSuivant] != Thread.CurrentThread.Name) { Monitor.Wait(peutEcrire); } // criture tableau for (int i = 0; i < data.Length; i++) { //attente 1 s Thread.Sleep(1000); // affichage data[i] = objRandom.Next(0, 1000); Console.WriteLine("{0:hh:mm:ss} : L'crivain {1} a crit le nombre {2}", DateTime.Now, Thread.CurrentThread.Name, data[i]); } // crivain suivant critureAutorise = false; crivainSuivant++; // on rveille les lecteurs en attente du jeton peutLire lock (peutLire) { lectureAutorise = true; Monitor.PulseAll(peutLire); } // suivi Console.WriteLine("Mthode [Ecrire] termine par le thread n {0}", Thread.CurrentThread.Name); } }

l'ensemble de l'accs au guichet d'criture est contrl par le lock des lignes 5-27. L'crivain qui rcupre le jeton le garde pendant tout son passage au guichet lignes 6-8 : un crivain ayant acquis le jeton ligne 5 le relche si l'criture n'est pas autorise ou si ce n'est pas son tour de passer. lignes 10-16 : passage au guichet (exploitation du tableau) lignes 18-19 : le thread change les conditions d'accs au guichet d'criture. On notera qu'il a toujours le jeton d'criture et que ces modifications ne peuvent pas encore permettre un crivain de passer. lignes 21-24 : le thread change les conditions d'accs au guichet de lecture et prvient tous les lecteurs en attente que quelque chose a chang. ligne 27 : le lock se termine, le jeton peutEcrire est relch. Un thread d'criture pourrait alors l'acqurir ligne 5 mais il ne passerait pas la condition d'accs puisque le boolen critureAutorise est faux. Par ailleurs, tous les threads qui sont en attente du jeton peutEcrire le restent dans l'attente d'une nouvelle opration PulseAll(peutEcrire).

Un exemple d'excution est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. Mthode [Lire] dmarre par le thread n L0 Mthode [Lire] dmarre par le thread n L2 Mthode [Lire] dmarre par le thread n L1 Le lecteur L2 est en position 0 Le lecteur L1 est en position 1 Le lecteur L0 est en position 2 Mthode [Ecrire] dmarre par le thread n E0 Mthode [Ecrire] dmarre par le thread n E1

Les threads d'excution

292

9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37.

L'crivain E0 est en position 0 L'crivain E1 est en position 1 L'crivain E2 est en position 2 Fin de Main... Mthode [Ecrire] dmarre par le thread n E2 12:09:05 : L'crivain E0 a crit le nombre 815 12:09:06 : L'crivain E0 a crit le nombre 990 12:09:07 : L'crivain E0 a crit le nombre 563 Mthode [Ecrire] termine par le thread n E0 12:09:08 : Le lecteur L2 a lu le nombre 815 12:09:09 : Le lecteur L2 a lu le nombre 990 12:09:10 : Le lecteur L2 a lu le nombre 563 Mthode [Lire] termine par le thread n L2 12:09:11 : L'crivain E1 a crit le nombre 411 12:09:12 : L'crivain E1 a crit le nombre 11 12:09:13 : L'crivain E1 a crit le nombre 54 Mthode [Ecrire] termine par le thread n E1 12:09:14 : Le lecteur L1 a lu le nombre 411 12:09:15 : Le lecteur L1 a lu le nombre 11 12:09:16 : Le lecteur L1 a lu le nombre 54 Mthode [Lire] termine par le thread n L1 12:09:17 : L'crivain E2 a crit le nombre 698 12:09:18 : L'crivain E2 a crit le nombre 448 12:09:19 : L'crivain E2 a crit le nombre 472 Mthode [Ecrire] termine par le thread n E2 12:09:20 : Le lecteur L0 a lu le nombre 698 12:09:21 : Le lecteur L0 a lu le nombre 448 12:09:22 : Le lecteur L0 a lu le nombre 472 Mthode [Lire] termine par le thread n L0

8.7

Les pools de threads

Jusqu' maintenant, pour grer des threads : nous les avons crs par Thread T=new Thread(...) puis excuts par T.Start() Nous avons vu au chapitre "Bases de donnes" qu'avec certains SGBD il tait possible d'avoir des pools de connexions ouvertes : n connexions sont ouvertes au dmarrage du pool lorsqu'un thread demande une connexion, on lui donne l'une des connexions ouvertes du pool lorsque le thread ferme la connexion, elle n'est pas ferme mais rendue au pool L'usage d'un pool de connexions est transparent au niveau du code. L'intrt rside dans l'amlioration des performances : l'ouverture d'une connexion cote cher. Ici 10 connexions ouvertes peuvent servir des centaines de demandes. Un systme analogue existe pour les threads :

min threads sont crs au dmarrage du pool. La valeur de min est fixe avec la mthode ThreadPool.SetMinThreads(min1,min2). Un pool de threads peut tre utilis pour excuter des tches bloquantes ou non bloquantes dites asynchrones. Le premier paramtre min1 fixe le nombre de threads bloquants, le second min2 le nombre de threads asynchrones. Les valeurs actuelles de ces deux valeurs peuvent tre obtenues par ThreadPool.GetMinThreads(out min1,out min2). si ce nombre n'est pas suffisant, le pool va crer d'autres threads pour rpondre aux demande jusqu' la limite de max threads. La valeur de max est fixe avec la mthode ThreadPool.SetMaxThreads(max1,max2). Les deux paramtres ont la mme signification que dans la mthode SetMinThreads. Les valeurs actuelles de ces deux valeurs peuvent tre obtenues par ThreadPool.GetMaxThreads(out max1,out max2). Lorsque les max1 threads auront t atteints, les demandes de threads pour tches bloquantes seront mises en attente d'un thread libre dans le pool.

Un pool de threads offre divers avantages :


comme pour le pool de connexions, on conomise sur le temps de cration des threads : 10 threads peuvent servir des centaines de demandes. on scurise l'application : en fixant un nombre maximum de threads, on vite l'asphyxie de l'application par des demandes trop nombreuses. Celles-ci seront mises en file d'attente.

Pour donner une tche un thread du pool, on utilise l'une des deux mthodes : 1. ThreadPool.QueueWorkItem(WaitCallBack) 2. ThreadPool.QueueWorkItem(WaitCallBack,object)

Les threads d'excution

293

o WaitCallBack est toute mthode ayant la signature void WaitCallBack(object). La mthode 1 demande un thread d'excuter la mthode WaitCallBack sans lui passer de paramtre. La mthode 2 fait la mme chose mais en passant un paramtre de type object la mthode WaitCallBack. Voici un programme illustrant ces concepts :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. using System; using System.Threading; namespace Chap8 { class Program { public static void Main() { // init Thread courant Thread main = Thread.CurrentThread; // on fixe un nom au Thread main.Name = "Main"; // on utilise un pool de threads int min1, min2; // on fixe le nombre minimal de threads bloquants ThreadPool.GetMinThreads(out min1, out min2); Console.WriteLine("Nombre minimum de tches bloquantes dans le pool : {0}", min1); Console.WriteLine("Nombre minimum de tches asynchrones dans le pool : {0}", min2); ThreadPool.SetMinThreads(3, min2); ThreadPool.GetMinThreads(out min1, out min2); Console.WriteLine("Nombre minimum de tches bloquantes dans le pool : {0}", min1); // on fixe le nombre maximal de threads bloquants int max1, max2; ThreadPool.GetMaxThreads(out max1, out max2); Console.WriteLine("Nombre maximum de tches bloquantes dans le pool : {0}", max1); Console.WriteLine("Nombre maximum de tches asynchrones dans le pool : {0}", max2); ThreadPool.SetMaxThreads(5, max2); ThreadPool.GetMaxThreads(out max1, out max2); Console.WriteLine("Nombre maximum de tches bloquantes dans le pool : {0}", max1); // on excute 7 threads for (int i = 0; i < 7; i++) { // on lance l'excution du thread i dans un pool ThreadPool.QueueUserWorkItem(Sleep, new Data2 { Numro = i.ToString(), Dbut = DateTime.Now, Dure = i + 1 }); } // fin de main Console.Write("Tapez [entre] pour terminer le thread {0} {1:hh:mm:ss}", main.Name, DateTime.Now); // attente Console.ReadLine(); } public static void Sleep(object infos) { // on rcupre le paramtre Data2 data = infos as Data2; Console.WriteLine("Le thread n {0} va dormir pendant {1} seconde(s)", data.Numro, data.Dure); // tat du pool int cpt1, cpt2; ThreadPool.GetAvailableThreads(out cpt1, out cpt2); Console.WriteLine("Nombre de threads pour tches bloquantes disponibles dans le pool : {0}", cpt1); // mise en sommeil pendant Dure secondes Thread.Sleep(data.Dure * 1000); // fin d'excution data.Fin = DateTime.Now; Console.WriteLine("Le thread n {0} est termin. Il tait programm pour durer {1} seconde(s). Il a dur {2} seconde(s)", data.Numro, data.Dure, data.Fin - data.Dbut); } }

53. 54. 55. 56. internal class Data2 { 57. // informations diverses 58. public string Numro { get; set; } 59. public DateTime Dbut { get; set; } 60. public int Dure { get; set; } 61. public DateTime Fin { get; set; } 62. } 63. }

Les threads d'excution

294

ligne 15-17 : on demande et affiche le nombre minimal actuel des deux types de threads du pool de threads ligne 18 : on change le nombre minimal de threads pour tches bloquantes : 2 lignes 19-21 : on affiches les nouveaux minima lignes 22-28 : on fait de mme pour fixer le nombre maximal de threads pour tches bloquantes : 5 lignes 30-33 : on fait excuter 7 tches dans un pool de 5 threads. 5 tches devraient obtenir 1 thread, les 2 premires rapidement puisque 2 threads sont toujours prsents, les 3 autres avec un dlai d'attente de 0.5 seconde. 2 tches devraient attendre qu'un thread se libre. ligne 32 : les tches excutent la mthode Sleep des lignes 40-54 en lui passant un paramtre de type Data2 dfini lignes 56-62. ligne 40 : la mthode Sleep excute par les tches ligne 42 : on rcupre le paramtre pass la mthode Sleep. ligne 43 : la tche s'identifie sur la console lignes 45-47 : on affiche le nombre de threads actuellement disponibles. On veut voir comment il volue. ligne 49 : la tche s'arrte quelques secondes (tche bloquante). ligne 52 : lorsqu'elle se rveille, on fait afficher quelques informations sur son compte.

Les rsultats obtenus sont les suivants. Pour les nombres min et max de threads dans le pool :
1. 2. 3. 4. 5. 6. Nombre Nombre Nombre Nombre Nombre Nombre minimum minimum minimum maximum maximum maximum de de de de de de tches tches tches tches tches tches bloquantes dans le pool : 2 asynchrones dans le pool : 2 bloquantes dans le pool aprs changement : 3 bloquantes dans le pool : 500 asynchrones dans le pool : 1000 bloquantes dans le pool aprs changement : 5

Pour l'excution des 7 threads :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. A 03:07:37:04, le thread n 0 Nombre de threads pour tches A 03:07:37:04, le thread n 2 Nombre de threads pour tches A 03:07:37:04, le thread n 1 Nombre de threads pour tches A 03:07:38:04, le thread n 3 Nombre de threads pour tches A 03:07:38:54, le thread n 4 Nombre de threads pour tches A 03:07:47:04, le thread n 0 00:00:10 seconde(s) A 03:07:47:04, le thread n 5 Nombre de threads pour tches A 03:07:48:04, le thread n 1 00:00:11 seconde(s) A 03:07:48:04, le thread n 6 Nombre de threads pour tches A 03:07:49:04, le thread n 2 00:00:12 seconde(s) A 03:07:51:04, le thread n 3 00:00:14 seconde(s) A 03:07:52:54, le thread n 4 00:00:15.5000000 seconde(s) A 03:08:02:04, le thread n 5 00:00:25 seconde(s) A 03:08:04:04, le thread n 6 00:00:27 seconde(s) va dormir pendant 10 seconde(s) bloquantes disponibles dans le pool va dormir pendant 12 seconde(s) bloquantes disponibles dans le pool va dormir pendant 11 seconde(s) bloquantes disponibles dans le pool va dormir pendant 13 seconde(s) bloquantes disponibles dans le pool va dormir pendant 14 seconde(s) bloquantes disponibles dans le pool se termine. Il tait programm pour : 3 : 2 : 2 : 1 : 0 durer 10 seconde(s). Il a dur

va dormir pendant 15 seconde(s) bloquantes disponibles dans le pool : 0 se termine. Il tait programm pour durer 11 seconde(s). Il a dur va dormir pendant 16 seconde(s) bloquantes disponibles dans le pool : 0 se termine. Il tait programm pour durer 12 seconde(s). Il a dur se termine. Il tait programm pour durer 13 seconde(s). Il a dur se termine. Il tait programm pour durer 14 seconde(s). Il a dur se termine. Il tait programm pour durer 15 seconde(s). Il a dur se termine. Il tait programm pour durer 16 seconde(s). Il a dur

lignes 1-6 : les 3 premires tches sont excutes tour tour. Elles trouvent imdiatement 1 thread disponible (MinThreads=3) puis se mette en sommeil. lignes 7-9 : pour les tches 3 et 4, c'est un peu plus long. Pour chacun d'eux il n'y avait pas de thread libre. Il a fallu en crer un. Ce mcanisme est possible jusqu' 5 (MaxThreads=5). ligne 10 : il n'y a plus de threads disponibles : les tches 5 et 6 vont devoir attendre. lignes 11-12 : la tche 0 se termine. La tche 5 prend son thread. lignes 13-14 : la tche 1 se termine. La tche 6 prend son thread. lignes 17-21 : les tches se terminent les unes aprs les autres.

Les threads d'excution

295

8.8
8.8.1

La classe BackgroundWorker
Exemple 1

La classe BackgroundWorker appartient l'espace de noms [System.ComponentModel]. Elle s'utilise comme un thread mais prsente des particularits qui peuvent la rendre, dans certains cas, plus intressante que la classe [Thread] :

elle met les vnements suivants : DoWork : un thread a demand l'excution du BackgroundWorker ProgressChanged : l'objet BackgroundWorker a excut la mthode ReportProgress. Celle-ci sert donner un pourcentage d'excution. RunWorkerCompleted : l'objet BackgroundWorker a termin son travail. Il a pu le terminer normalement ou sur annulation ou exception. Ces vnements rendent le BackgroundWorker utile dans les interfaces graphiques : une tche longue sera confie un BackgroundWorker qui pourra rendre compte de son avancement avec l'vnement ProgressChanged et sa fin avec l'vnement RunWorkerCompleted. Le travail effectuer par le BackgroundWorker sera gfait par une mthode qui aura t associe l'vnement DoWork. il est possible de demander son annulation. Dans une interface graphique, une tche longue pourra ainsi tre annule par l'utilisateur. les objets BackgroundWorker appartiennent un pool et sont recycls selon les besoins. Une application qui a besoin d'un objet BackgroundWorker l'obtiendra auprs du pool qui lui donnera un thread dj existant mais inutilis. Le fait de recycler ainsi les threads plutt que de crer chaque fois un thread neuf, amliore les performances.

Nous utilisons cet outil sur l'application prcdente dans le cas o l'accs au guichet est non contrl :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. using System; using System.Threading; using System.ComponentModel; namespace Chap8 { class Program2 { // utilisation de threads lecteurs et crivains // illustre l'utilisation simultane de ressources partages et de synchronisation // variables de classe const int nbThreads = 2; // nbre de threads au total static int nbLecteursTermins = 0; // nbre de threads termins static int[] data = new int[5]; // tableau partag entre threads lecteur et threads crivain static object appli; // synchronise l'accs au nbre de threads termins static Random objRandom = new Random(DateTime.Now.Second); // un gnrateur de nombres alatoires static AutoResetEvent peutLire; // signale qu'on peut lire le contenu du tableau static AutoResetEvent peutEcrire; // signale qu'on peut crire dans le tableau static AutoResetEvent finLecteurs; // signale la fin des lecteurs //main public static void Main(string[] args) { // on donne un nom au thread Thread.CurrentThread.Name = "Main"; // initialisation des drapeaux peutLire = new AutoResetEvent(false); // on ne peut pas encore lire peutEcrire = new AutoResetEvent(true); // on peut dj crire finLecteurs = new AutoResetEvent(false); // appli non termine // synchronise l'accs au compteur de threads termins appli = new object(); // cration des threads lecteurs MyBackgroundWorker[] lecteurs = new MyBackgroundWorker[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration lecteurs[i] = new MyBackgroundWorker(); lecteurs[i].Numro = "L" + i; lecteurs[i].DoWork += Lire; lecteurs[i].RunWorkerCompleted += EndLecteur; // lancement lecteurs[i].RunWorkerAsync(); } // cration des threads crivains MyBackgroundWorker[] crivains = new MyBackgroundWorker[nbThreads]; for (int i = 0; i < nbThreads; i++) { // cration crivains[i] = new MyBackgroundWorker(); crivains[i].Numro = "E" + i;

Les threads d'excution

296

52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84.

crivains[i].DoWork += Ecrire; // lancement crivains[i].RunWorkerAsync(); } // attente de la fin de tous les threads finLecteurs.WaitOne(); //fin de main Console.WriteLine("Fin de Main..."); } ... public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) { } // lire le contenu du tableau public static void Lire(object sender, DoWorkEventArgs infos) { ... } // crire dans le tableau public static void Ecrire(object sender, DoWorkEventArgs infos) { }

... }

// thread internal class MyBackgroundWorker : BackgroundWorker { // informations diverses public string Numro { get; set; } } }

Nous ne dtaillons que les changements :

la classe Thread est remplace par la classe MyBackgroundWorker des lignes 79-82. La classe BackgroundWorker a t drive afin de donner un numro au thread. On aurait pu procder diffremment en passant un objet la mthode RunWorkerAsync des lignes 43 et 54, objet contenant le n du thread. ligne 58 : la mthode Main se termine aprs que tous les threads lecteurs ont fait leur travail. Pour cela, ligne 12, le compteur nbLecteursTermins compte le nombre de threads lecteurs ayant termin leur travail. Ce compteur est incrment par la mthode EndLecteur des lignes 63-65 qui est excute chaque fois qu'un thread lecteur se termine. C'est cette procdure qui contrle l'vnement AutoResetEvent finLecteurs de la ligne 18 sur lequel se synchronise, ligne 59, la mthode Main. ligne 16 : parce que plusieurs threads lecteurs peuvent vouloir incrmenter en mme temps le compteur nbLecteursTermins, un accs exclusif celui-ci est assur par l'objet de synchronisation appli. Ce cas est improbable mais thoriquement possible. lignes 35-44 : cration des threads lecteurs ligne 38 : cration du thread de type MyBackgroundWorker ligne 39 : on lui donne un N ligne 40 : on lui assigne la mthode Lire excuter ligne 41 : la mthode EndLecteur sera excute aprs la fin du thread ligne 43 : le thread est lanc lignes 47-55 : cration des threads crivains ligne 50 : cration du thread de type MyBackgroundWorker ligne 51 : on lui donne un N ligne 52 : on lui assigne la mthode Ecrire excuter ligne 54 : le thread est lanc

Les mthodes Lire et Ecrire restent inchanges. La mthode EndLecteur est excute la fin de chaque thread lecteur. Son code est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. public static void EndLecteur(object sender, RunWorkerCompletedEventArgs infos) { // incrmentation nbre de lecteurs termins lock (appli) { nbLecteursTermins++; if (nbLecteursTermins == nbThreads) finLecteurs.Set(); } }

Le rle de la mthode EndLecteur est d'avertir la mthode Main que tous les lecteurs ont fait leur travail.

ligne 4 : le compteur nbLecteursTermins est incrment.

Les threads d'excution

297

lignes 5-6 : si tous les lecteurs ont fait leur travail, alors l'vnement finLecteurs est positionn vrai afin de prvenir la mthode Main qui attend cet vnement. parce que la procdure EndLecteur est excute par plusieurs threads, la section critique prcdente est protge par la clause lock de la ligne 3.

L'excution donne des rsultats analogues ceux de la version utilisant des threads. 8.8.2 Exemple 2

Le code suivant illustre d'autres points de la classe BackgroundWorker :



1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53.

la possibilit d'annuler la tche la remonte d'une exception lance dans la tche le passage d'un paramtre d'E/S la tche
using System; using System.Threading; using System.ComponentModel; namespace Chap8 { class Program3 { // threads static BackgroundWorker[] tches = new BackgroundWorker[5]; public static void Main() { // init Thread courant Thread main = Thread.CurrentThread; // on fixe un nom au Thread main.Name = "Main"; // cration de threads for (int i = 0; i < tches.Length; i++) { // on cre le thread n i tches[i] = new BackgroundWorker(); // on l'initialise tches[i].DoWork += Sleep; tches[i].RunWorkerCompleted += End; tches[i].WorkerSupportsCancellation = true; // on le lance tches[i].RunWorkerAsync(new Data { Numro = i, Dbut = DateTime.Now, Dure = i + 1 }); } // on annule le dernier thread tches[4].CancelAsync(); // fin de main Console.WriteLine("Fin du thread {0}, tapez [entre] pour terminer...", main.Name); Console.ReadLine(); return;

} ...

public static void Sleep(object sender, DoWorkEventArgs infos) { } public static void End(object sender, RunWorkerCompletedEventArgs infos) { ... } internal class Data { // informations diverses public int Numro { get; set; } public DateTime Dbut { get; set; } public int Dure { get; set; } public DateTime Fin { get; set; } } } }

ligne 9 : le tableau de BackgroundWorker lignes 18-27 : cration des threads ligne 20 : cration du thread ligne 22 : le thread excutera la mthode Sleep des lignes 39-41

Les threads d'excution

298

ligne 23 : la mthode End des lignes 43-45 sera excute la fin du thread ligne 24 : le thread pourra tre annul ligne 26 : le thread est lanc avec un paramtre de type [Data], dfini lignes 49-52. Cet objet a les champs suivants : Numro (entre) : n du thread Dbut (entre) : heure de dbut d'excution du thread Dure (entre) : dure d'excution du Sleep Fin (sortie) : fin d'excution du thread ligne 29 : le thread n 4 est annul

Tous les threads excutent la mthode Sleep suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. } public static void Sleep(object sender, DoWorkEventArgs infos) { // on exploite le paramtre infos Data data = (Data)infos.Argument; // exception pour la tche n 3 if (data.Numro == 3) { throw new Exception("test...."); } // mise en sommeil pendant Dure secondes avec un arrt ttes les secondes for (int i = 1; i <= data.Dure && !tches[data.Numro].CancellationPending; i++) { // attente d'1 seconde Thread.Sleep(1000); } // fin d'excution data.Fin = DateTime.Now; // on initialise le rsultat infos.Result = data; infos.Cancel = tches[data.Numro].CancellationPending;

ligne 1 : la mthode Sleep a la signature standard des gestionnaires d'vnements. Elle reoit deux paramtres : sender : l'metteur de l'vnement, ici le BackgroundWorker qui excute la mthode infos : de type DoWorkEventArgs qui donne des informations sur l'vnement DoWork. Ce paramtre sert aussi bien transmettre des informations au thread qu' rcuprer ses rsultats. ligne 3 : le paramtre pass la mthode RunWorkerAsync de la tche est retrouv dans la proprit infos.Argument. lignes 5-7 : on lance une exception pour la tche n 3 lignes 9-12 : le thread "dort" Dure secondes par tranches d'une seconde afin de permettre le test d'annulation de la ligne 9. Cela simule un travail de longue dure au cours duquel le thread vrifierait rgulirement s'il existe une demande d'annulation. Pour indiquer qu'il a t annul, le thread doit mettre la proprit infos.Cancel vrai (ligne 17). ligne 16 : le thread peut rendre un rsultat au thread qui l'a lanc. Il place ce rsultat dans infos.Result.

Une fois termins, les threads excutent la mthode End suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. public static void End(object sender, RunWorkerCompletedEventArgs infos) { // on exploite le paramtre infos pour afficher le rsultat de l'excution // exception ? if (infos.Error != null) { Console.WriteLine("Le thread {1} a rencontr l'erreur suivante : {0}", infos.Error.Message, sender); } else if (!infos.Cancelled) { Data data = (Data)infos.Result; Console.WriteLine("Thread {0} termin : dbut {1:hh:mm:ss}, dure programme {2} s, fin {3:hh:mm:ss}, dure effective {4}", data.Numro, data.Dbut, data.Dure, data.Fin, (data.Fin - data.Dbut)); } else { Console.WriteLine("Thread {0} annul", sender); } }

ligne 1 : la mthode End a la signature standard des gestionnaires d'vnements. Elle reoit deux paramtres : sender : l'metteur de l'vnement, ici le BackgroundWorker qui excute la mthode infos : de type RunWorkerCompletedEventArgs qui donne des informations sur l'vnement RunWorkerCompleted. ligne 4 : le champ infos.Error de type Exception est renseign seulement si une exception s'est produite. ligne 7 : le champ infos.Cancelled de type boolen la valeur true si le thread a t annul. ligne 8 : s'il y a pas eu exception ou annulation, alors infos.Result est le rsultat du thread excut. Utiliser ce rsultat s'il y a eu annulation du thread ou si le thread a lanc une exception, provoque une exception. Ainsi lignes 5 et 13, on n'est pas capables d'afficher le n du thread annul ou qui a lanc une exception car ce n est dans infos.Result. Ce problme peut tre contourn en drivant la classe BackgroundWorker pour y mettre les informations changer entre le thread appelant et

Les threads d'excution

299

le thread appel comme il a t fait dans l'exemple prcdent. On utilise alors l'argument sender qui reprsente le BackgroundWorker au lieu de l'argument infos. Les rsultats d'excution sont les suivants :
1. 2. 3. 4. 5. 6. Fin du thread Main. Laissez les autres threads se terminer puis tapez [entre] pour terminer... Thread 0 termin : dbut 05:19:46, dure programme 1 s, fin 05:19:47, dure effective 00:00:01 Le thread System.ComponentModel.BackgroundWorker a rencontr l'erreur suivante : test.... Thread System.ComponentModel.BackgroundWorker annul Thread 1 termin : dbut 05:19:46, dure programme 2 s, fin 05:19:49, dure effective 00:00:03 Thread 2 termin : dbut 05:19:46, dure programme 3 s, fin 05:19:50, dure effective 00:00:04

8.9
8.9.1

Donnes locales un thread


Le principe

Considrons une application trois couches :


utilisateur 1 utilisateur 2 ...

Couche ui [ui]

Couche mtier [metier]

Couche d'accs aux donnes [dao]

Donnes

Supposons que l'application soit multi-utilisateurs, une application web par exemple. Chaque utilisateur est servi par un thread qui lui est ddi. La vie du thread est la suivante : 1. 2. 3. le thread est cr ou demand un pool de threads pour satisfaire une demande d'un utilisateur si cette demande ncessite des donnes, le thread va excuter une mthode de la couche [ui] qui va appeler une mthode de la couche [metier] qui va son tour appeler une mthode de la couche [dao]. le thread rend la rponse l'utilisateur. Il disparat ensuite ou il est recycl dans un pool de threads.

Dans l'opration 2, il peut tre intressant que le thread ait des donnes qui lui soient propres, c.a.d. non partages avec les autres threads. Ces donnes pourraient par exemple appartenir l'utilisateur particulier que le thread sert. Ces donnes pourraient alors tre utilises dans les diffrentes couches [ui, metier, dao]. La classe Thread permet ce scnario grce une sorte de dictionnaire priv o les cls seraient de type LocalDataStoreSlot : cre une entre dans le dictionnaire priv du thread pour la cl name.

associe la valeur data la cl name du dictionnaire priv du thread

rcupre la valeur associe la cl name du dictionnaire priv du thread

Un modle d'utilisation pourrait tre le suivant :

pour crer un couple (cl,valeur) associ au thread courant :


Thread.SetData(Thread.GetNamedDataSlot("cl"),valeur);

pour rcuprer la valeur associe cl :


Thread.GetData(Thread.GetNamedDataSlot("cl"));

Les threads d'excution

300

8.9.2

Application du principe

Considrons l'application trois couches suivantes :


utilisateur 1 utilisateur 2 ...

Couche ui [ui]

Couche mtier [metier]

Couche d'accs aux donnes [dao]

Donnes

Supposons que la couche [dao] gre une base d'articles et que son interface soit initialement la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. using System.Collections.Generic; namespace Chap8 { public interface IDao { int InsertArticle(Article article); List<Article> GetAllArticles(); void DeleteAllArticles(); } }

ligne 5 : pour insrer un article dans la base ligne 6 : pour rcuprer tous les articles de la base ligne 7 : pour supprimer tous les articles de la base

Ultrieurement, apparat le besoin d'une mthode pour insrer un tableau d'articles l'aide d'une transaction parce qu'on souhaite fonctionner en tout ou rien : soit tous les articles sont insrs soit aucun. On peut alors modifier l'interface pour intgrer ce nouveau besoin :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using System.Collections.Generic; namespace Chap8 { public interface IDao { int InsertArticle(Article article); void insertArticles(Article[] articles); List<Article> GetAllArticles(); void DeleteAllArticles(); } }

ligne 6 : pour ajouter un tableau d'articles dans la base

Ultrieurement, pour une autre application, apparat le besoin de supprimer une liste d'articles enregistre dans une liste, toujours dans une transaction. On voit que pour rpondre des besoins mtier diffrents, la couche [dao] va tre amene grossir. On peut prendre une autre voie :

ne mettre dans la couche [dao] que les oprations basiques InsertArticle, DeleteArticle, UpdateArticle, SelectArticle, SelectArticles dporter dans la couche [mtier] les oprations de mise jour simultane de plusieurs articles. Celles-ci utiliseraient les oprations lmentaires de la couche [dao].

L'avantage de cette solution est que la mme couche [dao] pourrait tre utilise sans changement avec diffrentes couches [metier]. Elle amne une difficult dans la gestion de la transaction qui regroupe des mises jour faire de faon atomique sur la base : la transaction doit tre initie par la couche [metier] avant qu'elle n'appelle les mthodes de la couche [dao] les mthodes de la couche [dao] doivent connatre l'existence de la transaction afin d'y prendre part si elle existe la transaction doit ter termine par la couche [mtier]. Pour que les mthodes de la couche [dao] connaissent l'existence d'une ventuelle transaction en cours, on pourrait ajouter la transaction comme paramtre de chaque mthode de la couche [dao]. Ce paramtre va alors apparatre dans la signature des mthodes de l'interface, ce qui va lier celle-ci une source de donnes particulire : la base de donnes. Les donnes locales du thread nous apportent une solution plus lgante : la couche [mtier] mettra la transaction dans les donnes locales du thread et c'est l que la couche [dao] ira la chercher. La signature des mthodes de la couche [dao] n'a alors pas besoin d'tre change.

Les threads d'excution

301

Nous mettons en oeuvre cette solution avec le projet Visual studio suivant :
utilisateur 1 utilisateur 2 ...

Couche ui [ui]

Couche mtier [metier]

Couche d'accs aux donnes [dao]

Donnes

en [1] : la solution dans son ensemble en [2] : les rfrences utilises. La base [4] tant une base SQL Server Compact, il est ncessaire d'avoir la rfrence [System.Data.SqlServerCe]. en [3] : les diffrentes couches de l'application.

La base [4] est la base SQL Server Compact dj utilise dans le chapitre prcdent notamment au paragraphe 7.3.1, page 219.

La classe Article Une ligne de la table [articles] prcdente est encapsule dans un objet de type Article :
1. namespace Chap8 { 2. public class Article { 3. // proprits 4. public int Id { get; set; } 5. public string Nom { get; set; } 6. public decimal Prix { get; set; } 7. public int StockActuel { get; set; } 8. public int StockMinimum { get; set; } 9. 10. // constructeurs 11. public Article() { 12. } 13. 14. public Article(int id, string nom, decimal prix, int stockActuel, int stockMinimum) { 15. Id = id; 16. Nom = nom; 17. Prix = prix; 18. StockActuel = stockActuel; 19. StockMinimum = stockMinimum; 20. } 21.

Les threads d'excution

302

22. 23. 24. 25. 26. } 27. }

// identit public override string ToString() { return string.Format("[{0},{1},{2},{3},{4}]", Id, Nom, Prix, StockActuel, StockMinimum); }

Interface de la couche [dao] L'interface IDao de la couche [dao] sera la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. using System.Collections.Generic; namespace Chap8 { public interface IDao { int InsertArticle(Article article); List<Article> GetAllArticles(); void DeleteAllArticles(); } }

ligne 5 : pour insrer un article dans la table [articles] ligne 6 : pour mettre toutes les lignes de la table [articles] dans une liste d'objets Article ligne 7 : pour supprimer toutes les lignes de la table [articles]

Interface de la couche [metier] L'interface IMetier de la couche [metier] sera la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using System.Collections.Generic; namespace Chap8 { interface IMetier { void InsertArticlesInTransaction(Article[] articles); void InsertArticlesOutOfTransaction(Article[] articles); List<Article> GetAllArticles(); void DeleteAllArticles(); } }

ligne 5 : pour insrer, l'intrieur d'une transaction, un ensemble d'articles ligne 6 : idem mais sans transaction ligne 7 : pour obtenir la liste de tous les articles ligne 8 : pour supprimer tous les articles

Implmentation de la couche [metier] L'implmentation Metier de l'interface IMetier sera la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. using using using using System.Collections.Generic; System.Data; System.Data.SqlServerCe; System.Threading;

namespace Chap8 { public class Metier : IMetier { // couche [dao] public IDao Dao { get; set; } // chane de connexion public string ConnectionString { get; set; } // insertion d'un tableau d'articles l'intrieur d'une transaction public void InsertArticlesInTransaction(Article[] articles) { // on cre la connexion la base using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) { // ouverture connexion connexion.Open(); // transaction SqlCeTransaction transaction = null; try { // dbut transaction transaction = connexion.BeginTransaction(IsolationLevel.ReadCommitted);

Les threads d'excution

303

24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. } 57. }

// on enregistre la transaction dans le thread Thread.SetData(Thread.GetNamedDataSlot("transaction"), transaction); // insertion des articles foreach (Article article in articles) { Dao.InsertArticle(article); } // on valide la transaction transaction.Commit(); } catch { // on dfait la transaction if (transaction != null) transaction.Rollback(); } } }

// insertion d'un tableau d'articles sans transaction public void InsertArticlesOutOfTransaction(Article[] articles) { // insertion des articles foreach (Article article in articles) { Dao.InsertArticle(article); } } // liste des articles public List<Article> GetAllArticles() { return Dao.GetAllArticles(); } // supprimer tous les articles public void DeleteAllArticles() { Dao.DeleteAllArticles(); }

La classe a les proprits suivantes :


ligne 9 : une rfrence sur la couche [dao] ligne 11 : la chane de connexion qui permet de se connecter la base de donnes des articles

Nous ne commentons que la mthode InsertArticlesInTransaction qui seule prsente des difficults :

ligne 16 : une connexion avec la base est cre ligne 18 : elle est ouverte ligne 23 : une transaction est cre ligne 25 : elle est enregistre dans les donnes locales du thread, associe la cl "transaction" lignes 27-29 : la mthode d'insertion unitaire de la couche [dao] est appele pour chaque article insrer lignes 21 et 32 : l'ensemble de l'insertion du tableau est contrle par un try / catch ligne 31 : si on arrive l, c'est qu'il n'y a pas eu d'exception. On valide alors la transaction. lignes 34-35 : il y a eu exception, on dfait la transaction ligne 37 : on sort de la clause using. La connexion ouverte en ligne 18 est automatiquement ferme.

Implmentation de la couche [dao] L'implmentation Dao de l'interface IDao sera la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. using using using using System.Collections.Generic; System.Data; System.Data.SqlServerCe; System.Threading;

namespace Chap8 { public class Dao : IDao { // chane de connexion public string ConnectionString { get; set; } // requtes public string InsertText { get; set; } public string DeleteAllText { get; set; } public string GetAllText { get; set; } // implmentation interface // insertion article

Les threads d'excution

304

18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65.

public int InsertArticle(Article article) { // y-a-t-il une transaction en cours ? SqlCeTransaction transaction = Thread.GetData(Thread.GetNamedDataSlot("transaction")) as SqlCeTransaction; // rcuprer la connexion ou la crer SqlCeConnection connexion = null; if (transaction != null) { // rcuprer la connexion connexion = transaction.Connection as SqlCeConnection; } else { // la crer connexion = new SqlCeConnection(ConnectionString); connexion.Open(); } try { // prparation commande d'insertion SqlCeCommand sqlCommand = new SqlCeCommand(); sqlCommand.Transaction = transaction; sqlCommand.Connection = connexion; sqlCommand.CommandText = InsertText; sqlCommand.Parameters.Add("@nom", SqlDbType.NVarChar, 30); sqlCommand.Parameters.Add("@prix", SqlDbType.Money); sqlCommand.Parameters.Add("@sa", SqlDbType.Int); sqlCommand.Parameters.Add("@sm", SqlDbType.Int); sqlCommand.Parameters["@nom"].Value = article.Nom; sqlCommand.Parameters["@prix"].Value = article.Prix; sqlCommand.Parameters["@sa"].Value = article.StockActuel; sqlCommand.Parameters["@sm"].Value = article.StockMinimum; // excution return sqlCommand.ExecuteNonQuery(); } finally { // si on n'tait pas dans une transaction, on ferme la connexion if (transaction == null) { connexion.Close(); } } } // liste des articles public List<Article> GetAllArticles() { } // suppression des articles public void DeleteAllArticles() { ... } } }

...

La classe a les proprits suivantes :


ligne 9 : la chane de connexion qui permet de se connecter la base de donnes des articles ligne 11 : l'ordre SQL pour insrer un article ligne 12 : l'ordre SQL pour suprrimer tous les articles ligne 13 : l'ordre SQL pour obtenir tous les articles

Ces proprits seront initialises partir du fichier de configuration [App.config] suivant :


(a) <?xml version="1.0" encoding="utf-8" ?> (b) <configuration> (c) <connectionStrings> (d) <add name="dbArticlesSqlServerCe" connectionString="Data Source=| DataDirectory|\dbarticles.sdf;Password=dbarticles;" /> (e) </connectionStrings> (f) <appSettings> (g) <add key="insertText" value="insert into articles(nom,prix,stockactuel,stockminimum) values(@nom,@prix,@sa,@sm)"/> (h) <add key="getAllText" value="select id,nom,prix,stockactuel,stockminimum from articles"/> (i) <add key="deleteAllText" value="delete from articles"/> (j) </appSettings> (k) </configuration>

Nous commentons la mthode InsertArticle :

Les threads d'excution

305

ligne 20 : on rcupre l'ventuelle transaction qu'a pu placer la couche [metier] dans le thread lignes 23-25 : si la transaction est prsente, on rcupre la connexion laquelle elle a t lie. lignes 26-30 : sinon, une connexion nouvelle est cre et ouverte. lignes 33-44 : on prpare la commande d'insertion. Celle-ci est paramtre (cf ligne g de App.config). ligne 33 : l'objet Command est cr. ligne 34 : il est associ la transaction courante. Si celle-ci n'existe pas (transaction=null), cela revient excuter l'ordre SQL sans transaction explicite. On rappelle qu'alors il y a quand mme une transaction implicite. Avec SQL Server CE, cette transaction implicite est par dfaut en mode autocommit : l'ordre SQL est committ aprs son excution. ligne 35 : l'objet Command est associ la connexion courante ligne 36 : le texte SQl excuter est fix. C'est la requte paramtre de la ligne g de App.config. lignes 37-44 : les 4 paramtres de la requte sont initialiss ligne 46 : la requte est excute. lignes 49-51 : il faut se souvenir que s'il n'y avait pas de transaction, une nouvelle connexion a t ouverte avec la base, lignes 26-30. Dans ce cas, elle doit tre ferme. S'il y avait une transaction, la connexion ne doit pas tre ferme car c'est la couche [metier] qui la gre.

Les deux autres mthodes reprennent ce qui a t vu dans le chapitre "Bases de donnes" :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. // liste des articles public List<Article> GetAllArticles() { // liste des articles - vide au dpart List<Article> articles = new List<Article>(); // exploitation connexion using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) { // ouverture connexion connexion.Open(); // excute sqlCommand avec requte select SqlCeCommand sqlCommand = new SqlCeCommand(GetAllText, connexion); using (SqlCeDataReader reader = sqlCommand.ExecuteReader()) { // exploitation rsultat while (reader.Read()) { // exploitation ligne courante articles.Add(new Article(reader.GetInt32(0), reader.GetString(1), reader.GetDecimal(2), reader.GetInt32(3), reader.GetInt32(4))); } } } // on rend le rsultat return articles; } // suppression des articles public void DeleteAllArticles() { using (SqlCeConnection connexion = new SqlCeConnection(ConnectionString)) { // ouverture connexion connexion.Open(); // excute sqlCommand avec requte de mise jour new SqlCeCommand(DeleteAllText, connexion).ExecuteNonQuery(); }

16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. }

L'application [console] de test L'application [console] de test est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. using System; using System.Configuration;

namespace Chap8 { class Program { static void Main(string[] args) { // exploitation du fichier de configuration string connectionString = null; string insertText; string getAllText; string deleteAllText; try { // chane de connexion connectionString = ConfigurationManager.ConnectionStrings["dbArticlesSqlServerCe"].ConnectionString; 15. // autres paramtres 16. insertText = ConfigurationManager.AppSettings["insertText"]; 17. getAllText = ConfigurationManager.AppSettings["getAllText"];

Les threads d'excution

306

18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. } 70. }

deleteAllText = ConfigurationManager.AppSettings["deleteAllText"]; } catch (Exception e) { Console.WriteLine("Erreur de configuration : {0}", e.Message); return; } // cration couche [dao] Dao dao = new Dao(); dao.ConnectionString = connectionString; dao.DeleteAllText = deleteAllText; dao.GetAllText = getAllText; dao.InsertText = insertText; // cration couche [mtier] Metier metier = new Metier(); metier.Dao = dao; metier.ConnectionString = connectionString; // on cre un tableau d'articles Article[] articles = new Article[2]; for (int i = 0; i < articles.Length; i++) { articles[i] = new Article(0, "article", 100, 10, 1); } // on supprime tous les articles Console.WriteLine("Suppression de tous les articles..."); metier.DeleteAllArticles(); // on insre le tableau hors transaction Console.WriteLine("Insertion des articles hors transaction..."); try { metier.InsertArticlesOutOfTransaction(articles); } catch (Exception e){ Console.WriteLine("Exception : {0}", e.Message); } // on affiche les articles Console.WriteLine("Liste des articles"); AfficheArticles(metier); // on supprime tous les articles Console.WriteLine("Suppression de tous les articles..."); metier.DeleteAllArticles(); // on insre le tableau dans une transaction Console.WriteLine("Insertion des articles dans une transaction..."); metier.InsertArticlesInTransaction(articles); // on affiche les articles Console.WriteLine("Liste des articles"); AfficheArticles(metier);

private static void AfficheArticles(IMetier metier) { // on affiche les articles foreach(Article article in metier.GetAllArticles()){ Console.WriteLine(article); } }

lignes 12-22 : le fichier [App.config] est exploit. lignes 24-28 : la couche [dao] est instancie et initialise lignes 30-32 : il est fait de mme pour la couche [metier] lignes 34-37 : on cre un tableau de 2 articles avec le mme nom. La table [articles] de la base SQL server Ce [dbarticles.sdf] a une contrainte d'unicit sur le nom. L'insertion du 2ime article sera donc refuse. Si l'insertion du tableau se fait hors transaction, le 1er article sera d'abord insr puis le restera. Si l'insertion du tableau se fait dans une transaction, le 1er article sera d'abord insr puis sera retir, lors du Rollback de la transaction. lignes 39-50 : insertion hors transaction du tableau de 2 articles et vrification. lignes 52-59 : idem mais dans une transaction

Les rsultats l'excution sont les suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. Suppression de tous les articles... Insertion des articles hors transaction... Exception : A duplicate value cannot be inserted into a unique index. [ Table na me = ARTICLES,Constraint name = UQ__ARTICLES__0000000000000010 ] Liste des articles [7,article,100,10,1] Suppression de tous les articles... Insertion des articles dans une transaction... Liste des articles

Les threads d'excution

307

lignes 5-6 : l'insertion hors transaction a laiss le 1er article dans la base ligne 9 : l'insertion faite dans une transaction n'a laiss aucun article dans la base Conclusion

8.9.3

L'exemple prcdent a montr l'intrt des donnes locales un thread pour la gestion des transactions. Il n'est pas reproduire tel quel. Des frameworks tels que Spring, Nhibernate, ... utilisent cette technique mais la rendent encore plus transparente : il est possible pour la couche [metier] d'utiliser des transactions sans que la couche [dao] n'ait besoin de le savoir. Il n'y a alors aucun objet Transaction dans le code de la couche [dao]. Cela est obtenu au moyen d'une technique de proxy appele AOP (Aspects Oriented Programming). De nouveau on ne peut qu'inciter le lecteur utiliser ces frameworks.

8.10

Pour approfondir...

Pour approfondir le domaine difficile de la synchronisation de threads, on pourra lire le chapitre Threading du livre C# 3.0 rfrenc dans l'introduction de ce document. On y prsente de nombreuses techniques de synchronisation pour diffrents types de situation.

Les threads d'excution

308

9
9.1

Programmation Internet
Gnralits
Les protocoles de l'Internet

9.1.1

Nous donnons ici une introduction aux protocoles de communication de l'Internet, appels aussi suite de protocoles TCP/IP (Transfer Control Protocol / Internet Protocol), du nom des deux principaux protocoles. Il peut tre utile que le lecteur ait une comprhension globale du fonctionnement des rseaux et notamment des protocoles TCP/IP avant d'aborder la construction d'applications distribues. Le texte qui suit est une traduction partielle d'un texte que l'on trouve dans le document "Lan Workplace for Dos - Administrator's Guide" de NOVELL, document du dbut des annes 90. ----------------------------------Le concept gnral de crer un rseau d'ordinateurs htrognes vient de recherches effectues par le DARPA (Defense Advanced Research Projects Agency) aux Etats-Unis. Le DARPA a dvelopp la suite de protocoles connue sous le nom de TCP/IP qui permet des machines htrognes de communiquer entre elles. Ces protocoles ont t tests sur un rseau appel ARPAnet, rseau qui devint ultrieurement le rseau INTERNET. Les protocoles TCP/IP dfinissent des formats et des rgles de transmission et de rception indpendants de l'organisation des rseaux et des matriels utiliss. Le rseau conu par le DARPA et gr par les protocoles TCP/IP est un rseau commutation de paquets. Un tel rseau transmet l'information sur le rseau, en petits morceaux appels paquets. Ainsi, si un ordinateur transmet un gros fichier, ce dernier sera dcoup en petits morceaux qui seront envoys sur le rseau pour tre recomposs destination. TCP/IP dfinit le format de ces paquets, savoir :

origine du paquet destination longueur type

9.1.2

Le modle OSI

Les protocoles TCP/IP suivent peu prs le modle de rseau ouvert appel OSI (Open Systems Interconnection Reference Model) dfini par l'ISO (International Standards Organisation). Ce modle dcrit un rseau idal o la communication entre machines peut tre reprsente par un modle sept couches :
|-------------------------------------| | Application | |-------------------------------------| | Prsentation | |-------------------------------------| | Session | |-------------------------------------| | Transport | |-------------------------------------| | Rseau | |-------------------------------------| | Liaison | |-------------------------------------| | Physique | |-------------------------------------|

7 6 5 4 3 2 1

Chaque couche reoit des services de la couche infrieure et offre les siens la couche suprieure. Supposons que deux applications situes sur des machines A et B diffrentes veulent communiquer : elles le font au niveau de la couche Application. Elles n'ont pas besoin de connatre tous les dtails du fonctionnement du rseau : chaque application remet l'information qu'elle souhaite transmettre la couche du dessous : la couche Prsentation. L'application n'a donc connatre que les rgles d'interfaage avec la couche Prsentation.

Programmation Internet

309

Une fois l'information dans la couche Prsentation, elle est passe selon d'autres rgles la couche Session et ainsi de suite, jusqu' ce que l'information arrive sur le support physique et soit transmise physiquement la machine destination. L, elle subira le traitement inverse de celui qu'elle a subi sur la machine expditeur. A chaque couche, le processus expditeur charg d'envoyer l'information, l'envoie un processus rcepteur sur l'autre machine apartenant la mme couche que lui. Il le fait selon certaines rgles que l'on appelle le protocole de la couche. On a donc le schma de communication final suivant :
Machine A Machine B +-------------------------------------+ +----------------------------+ Application v ^ Application +------------------------------------ +--------------------------- Prsentation v ^ Prsentation +------------------------------------ +--------------------------- Session v ^ Session +------------------------------------ +--------------------------- Transport v ^ Transport +------------------------------------ +--------------------------- Rseau v ^ Rseau +------------------------------------ +--------------------------- Liaison v ^ Liaison +------------------------------------ +--------------------------- Physique v ^ Physique +------------------------------------+ +---------------------------+ ^ +-->------->------>-----+

7 6 5 4 3 2 1

Le rle des diffrentes couches est le suivant :


Physique

Assure la transmission de bits sur un support physique. On trouve dans cette couche des quipements terminaux de traitement des donnes (E.T.T.D.) tels que terminal ou ordinateur, ainsi que des quipements de terminaison de circuits de donnes (E.T.C.D.) tels que modulateur/dmodulateur, multiplexeur, concentrateur. Les points d'intrt ce niveau sont :

le choix du codage de l'information (analogique ou numrique) le choix du mode de transmission (synchrone ou asynchrone).

Liaison de donnes Rseau Transport

Masque les particularits physiques de la couche Physique. Dtecte et corrige les erreurs de transmission. Gre le chemin que doivent suivre les informations envoyes sur le rseau. On appelle cela le routage : dterminer la route suivre par une information pour qu'elle arrive son destinataire. Permet la communication entre deux applications alors que les couches prcdentes ne permettaient que la communication entre machines. Un service fourni par cette couche peut tre le multiplexage : la couche transport pourra utiliser une mme connexion rseau (de machine machine) pour transmettre des informations appartenant plusieurs applications. On va trouver dans cette couche des services permettant une application d'ouvrir et de maintenir une session de travail sur une machine distante. Elle vise uniformiser la reprsentation des donnes sur les diffrentes machines. Ainsi des donnes provenant d'une machine A, vont tre "habilles" par la couche Prsentation de la machine A, selon un format standard avant d'tre envoyes sur le rseau. Parvenues la couche Prsentation de la machine destinatrice B qui les reconnatra grce leur format standard, elles seront habilles d'une autre faon afin que l'application de la machine B les reconnaisse. A ce niveau, on trouve les applications gnralement proches de l'utilisateur telles que la messagerie lectronique ou le transfert de fichiers.

Session

Prsentation

Application

9.1.3

Le modle TCP/IP

Le modle OSI est un modle idal encore jamais ralis. La suite de protocoles TCP/IP s'en approche sous la forme suivante :

Programmation Internet

310

7 6 5 4 3 2 1

+-----------------------+ Application +----------------------- Prsentation +----------------------- Session +----------------------- Transport +----------------------- Rseau +----------------------- Liaison +----------------------- Physique +-----------------------+

+-------------------------------------------------------+ DNS Telnet FTP TFTP SMTP +------------------ Autres +-------------------------+----------------------------- TCP UDP +------------------------------------------------------- IP ICMP ARP RARP +------------------------------------------------------- MLID1 MLID2 MLID3 MLID4 +------------------------------------------------------- Ethernet Token-ring Autres +-------------------------------------------------------+

Couche Physique En rseau local, on trouve gnralement une technologie Ethernet ou Token-Ring. Nous ne prsentons ici que la technologie Ethernet. Ethernet C'est le nom donn une technologie de rseaux locaux commutation de paquets invente PARC Xerox au dbut des annes 1970 et normalise par Xerox, Intel et Digital Equipment en 1978. Le rseau est physiquement constitu d'un cble coaxial d'environ 1,27 cm de diamtre et d'une longueur de 500 m au plus. Il peut tre tendu au moyen de rpteurs, deux machines ne pouvant tre spares par plus de deux rpteurs. Le cble est passif : tous les lments actifs sont sur les machines raccordes au cble. Chaque machine est relie au cble par une carte d'accs au rseau comprenant :

un transmetteur (transceiver) qui dtecte la prsence de signaux sur le cble et convertit les signaux analogiques en signaux numrique et inversement. un coupleur qui reoit les signaux numriques du transmetteur et les transmet l'ordinateur pour traitement ou inversement.

Les caractristiques principales de la technologie Ethernet sont les suivantes :


Capacit de 10 Mgabits/seconde. Topologie en bus : toutes les machines sont raccordes au mme cble

--------------------------------------------------------- +--------+ +--------+ +-----------+ +--------+ +--------+ +-----------+ Machine A B C

Rseau diffusant - Une machine qui met transfre des informations sur le cble avec l'adresse de la machine destinatrice. Toutes les machines raccordes reoivent alors ces informations et seule celle qui elles sont destines les conserve. La mthode d'accs est la suivante : le transmetteur dsirant mettre coute le cble - il dtecte alors la prsence ou non d'une onde porteuse, prsence qui signifierait qu'une transmission est en cours. C'est la technique CSMA (Carrier Sense Multiple Access). En l'absence de porteuse, un transmetteur peut dcider de transmettre son tour. Ils peuvent tre plusieurs prendre cette dcision. Les signaux mis se mlangent : on dit qu'il y a collision. Le transmetteur dtecte cette situation : en mme temps qu'il met sur le cble, il coute ce qui passe rellement sur celui-ci. S'il dtecte que l'information transitant sur le cble n'est pas celle qu'il a mise, il en dduit qu'il y a collision et il s'arrtera d'mettre. Les autres transmetteurs qui mettaient feront de mme. Chacun reprendra son mission aprs un temps alatoire dpendant de chaque transmetteur. Cette technique est appele CD (Collision Detect). La mthode d'accs est ainsi appele CSMA/CD. un adressage sur 48 bits. Chaque machine a une adresse, appele ici adresse physique, qui est inscrite sur la carte qui la relie au cble. On appelle cet adresse, l'adresse Ethernet de la machine.

Couche Rseau Nous trouvons au niveau de cette couche, les protocoles IP, ICMP, ARP et RARP.

Programmation Internet

311

IP (Internet Protocol)

Dlivre des paquets entre deux noeuds du rseau ICMP ralise la communication entre le programme du protocole IP d'une machine et celui d'une autre machine. C'est donc un protocole d'change de messages l'intrieur mme du protocole IP. fait la correspondance adresse Internet machine--> adresse physique machine

ICMP (Internet Control Message Protocol)

ARP (Address Resolution Protocol)

RARP (Reverse Address Resolution Protocol)

fait la correspondance adresse physique machine--> adresse Internet machine

Couches Transport/Session Dans cette couche, on trouve les protocoles suivants :


TCP (Transmission Control Protocol) UDP (User Datagram Protocol)

Assure une remise fiable d'informations entre deux clients Assure une remise non fiable d'informations entre deux clients

Couches Application/Prsentation/Session On trouve ici divers protocoles :


TELNET

Emulateur de terminal permettant une machine A de se connecter une machine B en tant que terminal permet des transferts de fichiers permet des transferts de fichiers

FTP (File Transfer Protocol)

TFTP (Trivial File Transfer Protocol)

SMTP (Simple Mail Transfer protocol)

permet l'change de messages entre utilisateurs du rseau

DNS (Domain Name System)

transforme un nom de machine en adresse Internet de la machine cr par sun MicroSystems, il spcifie une reprsentation standard des donnes, indpendante des machines dfini galement par Sun, c'est un protocole de communication entre applications distantes, indpendant de la couche transport. Ce protocole est important : il dcharge le programmeur de la connaissance des dtails de la couche transport et rend les applications portables. Ce protocole s'appuie sur sur le protocole XDR toujours dfini par Sun, ce protocole permet une machine, de "voir" le systme de fichiers d'une autre machine. Il s'appuie sur le protocole RPC prcdent

XDR (eXternal Data Representation)

RPC(Remote Procedures Call)

NFS (Network File System)

9.1.4

Fonctionnement des protocoles de l'Internet

Les applications dveloppes dans l'environnement TCP/IP utilisent gnralement plusieurs des protocoles de cet environnement. Un programme d'application communique avec la couche la plus leve des protocoles. Celle-ci passe l'information la couche du

Programmation Internet

312

dessous et ainsi de suite jusqu' arriver sur le support physique. L, l'information est physiquement transfre la machine destinatrice o elle retraversera les mmes couches, en sens inverse cette fois-ci, jusqu' arriver l'application destinatrice des informations envoyes. Le schma suivant montre le parcours de l'information :
+----------------+ +---------------------------+ Application Application +----------------+ +---------------------------+ <----------- messages ou streams ----------> +----------------+ +---------------------------+ Transport Transport (Udp/Tcp) (Udp/tcp) +----------------+ +---------------------------+ <----------- datagrammes (UDP) -----------> +----------------+ ou +---------------------------+ Rseau (IP) segments (TCP) Rseau (IP) +----------------+ +---------------------------+ <----------- datagrammes IP --------------> +----------------+ +----------------------------+ Interface rseau Interface rseau +---------------+ +----------------------------+ <---------- trames rseau -------------> +----------------------------------------------+ rseau physique

Prenons un exemple : l'application FTP, dfinie au niveau de la couche Application et qui permet des transferts de fichiers entre machines.

L'application dlivre une suite d'octets transmettre la couche transport. La couche transport dcoupe cette suite d'octets en segments TCP, et ajoute au dbut de chaque segment, le numro de celuici. Les segments sont passs la couche Rseau gouverne par le protocole IP. La couche IP cre un paquet encapsulant le segment TCP reu. En tte de ce paquet, elle place les adresses Internet des machines source et destination. Elle dtermine galement l'adresse physique de la machine destinatrice. Le tout est pass la couche Liaison de donnes & Liaison physique, c'est dire la carte rseau qui couple la machine au rseau physique. L, le paquet IP est encapsul son tour dans une trame physique et envoy son destinataire sur le cble. Sur la machine destinatrice, la couche Liaison de donnes & Liaison physique fait l'inverse : elle dsencapsule le paquet IP de la trame physique et le passe la couche IP. La couche IP vrifie que le paquet est correct : elle calcule une somme, fonction des bits reus (checksum), somme qu'elle doit retrouver dans l'en-tte du paquet. Si ce n'est pas le cas, celui-ci est rejet. Si le paquet est dclar correct, la couche IP dsencapsule le segment TCP qui s'y trouve et le passe au-dessus la couche transport. La couche transport, couche TCP dans notre exemple, examine le numro du segment afin de restituer le bon ordre des segments. Elle calcule galement une somme de vrification pour le segment TCP. S'il est trouv correct, la couche TCP envoie un accus de rception la machine source, sinon le segment TCP est refus. Il ne reste plus la couche TCP qu' transmettre la partie donnes du segment l'application destinatrice de celles-ci dans la couche du dessus.

9.1.5

Les problmes d'adressage dans l'Internet

Un noeud d'un rseau peut tre un ordinateur, une imprimante intelligente, un serveur de fichiers, n'importe quoi en fait pouvant communiquer l'aide des protocoles TCP/IP. Chaque noeud a une adresse physique ayant un format dpendant du type du rseau. Sur un rseau Ethernet, l'adresse physique est code sur 6 octets. Une adresse d'un rseau X25 est un nombre 14 chiffres. L'adresse Internet d'un noeud est une adresse logique : elle est indpendante du matriel et du rseau utilis. C'est une adresse sur 4 octets identifiant la fois un rseau local et un noeud de ce rseau. L'adresse Internet est habituellement reprsente sous la forme de 4 nombres, valeurs des 4 octets, spars par un point. Ainsi l'adresse de la machine Lagaffe de la facult des Sciences d'Angers est note 193.49.144.1 et celle de la machine Liny 193.49.144.9. On en dduira que l'adresse Internet du rseau local est 193.49.144.0. On pourra avoir jusqu' 254 noeuds sur ce rseau. Parce que les adresses Internet ou adresses IP sont indpendantes du rseau, une machine d'un rseau A peut communiquer avec une machine d'un rseau B sans se proccuper du type de rseau sur lequel elle se trouve : il suffit qu'elle connaisse son adresse IP. Le protocole IP de chaque rseau se charge de faire la conversion adresse IP <--> adresse physique, dans les deux sens. Les adresses IP doivent tre toutes diffrentes. En France, c'est l'INRIA qui s'occupe d'affecter les adresses IP. En fait, cet organisme dlivre une adresse pour votre rseau local, par exemple 193.49.144.0 pour le rseau de la facult des sciences d'Angers.

Programmation Internet

313

L'administrateur de ce rseau peut ensuite affecter les adresses IP 193.49.144.1 193.49.144.254 comme il l'entend. Cette adresse est gnralement inscrite dans un fichier particulier de chaque machine relie au rseau.

9.1.5.1

Les classes d'adresses IP

Une adresse IP est une suite de 4 octets note souvent I1.I2.I3.I4, qui contient en fait deux adresses :

l'adresse du rseau l'adresse d'un noeud de ce rseau

Selon la taille de ces deux champs, les adresses IP sont divises en 3 classes : classes A, B et C. Classe A L'adresse IP : I1.I2.I3.I4 a la forme R1.N1.N2.N3 o R1 N1.N2.N3 est l'adresse du rseau est l'adresse d'une machine dans ce rseau

Plus exactement, la forme d'une adresse IP de classe A est la suivante :


1 octet 3 octets +-------------------------------------------------------------------------------+ 0 adr. rseau adresse noeud +-------------------------------------------------------------------------------+

L'adresse rseau est sur 7 bits et l'adresse du noeud sur 24 bits. On peut donc avoir 127 rseaux de classe A, chacun comportant jusqu' 224 noeuds. Classe B Ici, l'adresse IP : I1.I2.I3.I4 a la forme R1.R2.N1.N2 o R1.R2 N1.N2 est l'adresse du rseau est l'adresse d'une machine dans ce rseau

Plus exactement, la forme d'une adresse IP de classe B est la suivante :


2 octets 2 octets +-------------------------------------------------------------------------------+ 10 adresse rseau adresse noeud +-------------------------------------------------------------------------------+

L'adresse du rseau est sur 2 octets (14 bits exactement) ainsi que celle du noeud. On peut donc avoir 2 14 rseaux de classe B chacun comportant jusqu' 216 noeuds. Classe C Dans cette classe, l'adresse IP : I1.I2.I3.I4 a la forme R1.R2.R3.N1 o R1.R2.R3 N1 est l'adresse du rseau est l'adresse d'une machine dans ce rseau

Plus exactement, la forme d'une adresse IP de classe C est la suivante :


3 octets 1 octet +-------------------------------------------------------------------------------+ 110 adresse rseau adr. noeud +-------------------------------------------------------------------------------+

Programmation Internet

314

L'adresse rseau est sur 3 octets (moins 3 bits) et l'adresse du noeud sur 1 octet. On peut donc avoir 2 21 rseaux de classe C comportant jusqu' 256 noeuds. L'adresse de la machine Lagaffe de la facult des sciences d'Angers tant 193.49.144.1, on voit que l'octet de poids fort vaut 193, c'est dire en binaire 11000001. On en dduit que le rseau est de classe C. Adresses rserves

Certaines adresses IP sont des adresses de rseaux plutt que des adresses de noeuds dans le rseau. Ce sont celles, o l'adresse du noeud est mise 0. Ainsi, l'adresse 193.49.144.0 est l'adresse IP du rseau de la Facult des Sciences d'Angers. En consquence, aucun noeud d'un rseau ne peut avoir l'adresse zro. Lorsque dans une adresse IP, l'adresse du noeud ne comporte que des 1, on a alors une adresse de diffusion : cette adresse dsigne tous les noeuds du rseau. Dans un rseau de classe C, permettant thoriquement 28=256 noeuds, si on enlve les deux adresses interdites, on n'a plus que 254 adresses autorises.

9.1.5.2

Les protocoles de conversion Adresse Internet <--> Adresse physique

Nous avons vu que lors d'une mission d'informations d'une machine vers une autre, celles-ci la traverse de la couche IP taient encapsules dans des paquets. Ceux-ci ont la forme suivante :
. <---- En-tte paquet IP ---------------------------> . <---Donnes paquet IP ------------>. +---------------------------------------------------------------------------------------------+ Info Adresse Internet Adresse Internet Source Destination +---------------------------------------------------------------------------------------------+

Le paquet IP contient donc les adresses Internet des machines source et destination. Lorsque ce paquet va tre transmis la couche charge de l'envoyer sur le rseau physique, d'autres informations lui sont ajoutes pour former la trame physique qui sera finalement envoye sur le rseau. Par exemple, le format d'une trame sur un rseau Ethernet est le suivant :
. <---- En-tte trame Ethernet -----------------------> . <-Donnes trame Ethernet->. +----------------------------------------------------------------------------------------------------+ Info Adresse Physique Adresse Physique longueur Paquet IP Ethernet Source Destination paquet CRC +----------------------------------------------------------------------------------------------------+ 8 oct 6 6 2 46 1500 4

Dans la trame finale, il y a l'adresse physique des machines source et destination. Comment sont-elles obtenues ? La machine expditrice connaissant l'adresse IP de la machine avec qui elle veut communiquer obtient l'adresse physique de celle-ci en utilisant un protocole particulier appel ARP (Address Resolution Protocol).

Elle envoie un paquet d'un type spcial appel paquet ARP contenant l'adresse IP de la machine dont on cherche l'adresse physique. Elle a pris soin galement d'y placer sa propre adresse IP ainsi que son adresse physique. Ce paquet est envoy tous les noeuds du rseau. Ceux-ci reconnaissent la nature spciale du paquet. Le noeud qui reconnat son adresse IP dans le paquet, rpond en envoyant l'expditeur du paquet son adresse physique. Comment le peut-il ? Il a trouv dans le paquet les adresses IP et physique de l'expditeur. L'expditeur reoit donc l'adresse physique qu'il cherchait. Il la stocke en mmoire afin de pouvoir l'utiliser ultrieurement si d'autres paquets sont envoyer au mme destinataire.

L'adresse IP d'une machine est normalement inscrite dans l'un de ses fichiers qu'elle peut donc consulter pour la connatre. Cette adresse peut tre change : il suffit d'diter le fichier. L'adresse physique elle, est inscrite dans une mmoire de la carte rseau et ne peut tre change. Lorsqu'un administrateur dsire d'organiser son rseau diffremment, il peut tre amen changer les adresses IP de tous les noeuds et donc diter les diffrents fichiers de configuration des diffrents noeuds. Cela peut tre fastidieux et une occasion d'erreurs s'il y a beaucoup de machines. Une mthode consiste ne pas affecter d'adresse IP aux machines : on inscrit alors un code spcial dans le fichier dans lequel la machine devrait trouver son adresse IP. Dcouvrant qu'elle n'a pas d'adresse IP, la machine la demande selon un protocole appel RARP (Reverse Address Resolution Protocol). Elle envoie alors sur un rseau un paquet spcial appel paquet RARP, analogue au paquet ARP prcdent, dans lequel elle met son adresse physique. Ce paquet est envoy tous les noeuds qui reconnaissent alors un paquet RARP. L'un d'entre-eux, appel serveur RARP, possde un fichier donnant la correspondance adresse physique <--> adresse IP de tous les noeuds. Il rpond alors l'expditeur du paquet RARP, en lui

Programmation Internet

315

renvoyant son adresse IP. Un administrateur dsirant reconfigurer son rseau, n'a donc qu' diter le fichier de correspondances du serveur RARP. Celui-ci doit normalement avoir une adresse IP fixe qu'il doit pouvoir connatre sans avoir utiliser lui-mme le protocole RARP.

9.1.6

La couche rseau dite couche IP de l'internet

Le protocole IP (Internet Protocol) dfinit la forme que les paquets doivent prendre et la faon dont ils doivent tre grs lors de leur mission ou de leur rception. Ce type de paquet particulier est appel un datagramme IP. Nous l'avons dj prsent :
. <---- En-tte paquet IP ---------------------------> . <---Donnes paquet IP ------------>. +---------------------------------------------------------------------------------------------+ Info Adresse Internet Adresse Internet Source Destination +---------------------------------------------------------------------------------------------+

L'important est qu'outre les donnes transmettre, le datagramme IP contient les adresses Internet des machines source et destination. Ainsi la machine destinatrice sait qui lui envoie un message. A la diffrence d'une trame de rseau qui a une longueur dtermine par les caractristiques physiques du rseau sur lequel elle transite, la longueur du datagramme IP est elle fixe par le logiciel et sera donc la mme sur diffrents rseaux physiques. Nous avons vu qu'en descendant de la couche rseau dans la couche physique le datagramme IP tait encapsul dans une trame physique. Nous avons donn l'exemple de la trame physique d'un rseau Ethernet :
. <---- En-tte trame Ethernet -------------------------------->. <---Donnes trame Ethernet------>. +----------------------------------------------------------------------------------------------------+ Info Adresse Physique Adresse Physique Type du Paquet IP Ethernet Source Destination paquet CRC +----------------------------------------------------------------------------------------------------+

Les trames physiques circulent de noeud en noeud vers leur destination qui peut ne pas tre sur le mme rseau physique que la machine expditrice. Le paquet IP peut donc tre encapsul successivement dans des trames physiques diffrentes au niveau des noeuds qui font la jonction entre deux rseaux de type diffrent. Il se peut aussi que le paquet IP soit trop grand pour tre encapsul dans une trame physique. Le logiciel IP du noeud o se pose ce problme, dcompose alors le paquet IP en fragments selon des rgles prcises, chacun d'eux tant ensuite envoy sur le rseau physique. Ils ne seront rassembls qu' leur ultime destination.

9.1.6.1

Le routage

Le routage est la mthode d'acheminement des paquets IP leur destination. Il y a deux mthodes : le routage direct et le routage indirect. Routage direct Le routage direct dsigne l'acheminement d'un paquet IP directement de l'expditeur au destinataire l'intrieur du mme rseau :

La machine expditrice d'un datagramme IP a l'adresse IP du destinataire. Elle obtient l'adresse physique de ce dernier par le protocole ARP ou dans ses tables, si cette adresse a dj t obtenue. Elle envoie le paquet sur le rseau cette adresse physique.

Routage indirect Le routage indirect dsigne l'acheminement d'un paquet IP une destination se trouvant sur un autre rseau que celui auquel appartient l'expditeur. Dans ce cas, les parties adresse rseau des adresses IP des machines source et destination sont diffrentes. La machine source reconnat ce point. Elle envoie alors le paquet un noeud spcial appel routeur (router), noeud qui connecte un rseau local aux autres rseaux et dont elle trouve l'adresse IP dans ses tables, adresse obtenue initialement soit dans un fichier soit dans une mmoire permanente ou encore via des informations circulant sur le rseau. Un routeur est attach deux rseaux et possde une adresse IP l'intrieur de ces deux rseaux.
+------------+ rseau 2 routeur rseau 1 ----------------|193.49.144.6|-----------193.49.145.0 193.49.145.3 193.49.144.0 +------------+

Programmation Internet

316

Dans notre exemple ci-dessus : . . Le rseau n 1 a l'adresse Internet 193.49.144.0 et le rseau n 2 l'adresse 193.49.145.0. A l'intrieur du rseau n 1, le routeur a l'adresse 193.49.144.6 et l'adresse 193.49.145.3 l'intrieur du rseau n 2.

Le routeur a pour rle de mettre le paquet IP qu'il reoit et qui est contenu dans une trame physique typique du rseau n 1, dans une trame physique pouvant circuler sur le rseau n 2. Si l'adresse IP du destinataire du paquet est dans le rseau n 2, le routeur lui enverra le paquet directement sinon il l'enverra un autre routeur, connectant le rseau n 2 un rseau n 3 et ainsi de suite.

9.1.6.2

Messages d'erreur et de contrle

Toujours dans la couche rseau, au mme niveau donc que le protocole IP, existe le protocole ICMP (Internet Control Message Protocol). Il sert envoyer des messages sur le fonctionnement interne du rseau : noeuds en panne, embouteillage un routeur, etc ... Les messages ICMP sont encapsuls dans des paquets IP et envoys sur le rseau. Les couches IP des diffrents noeuds prennent les actions appropries selon les messages ICMP qu'elles reoivent. Ainsi, une application elle-mme, ne voit jamais ces problmes propres au rseau. Un noeud utilisera les informations ICMP pour mettre jour ses tables de routage.

9.1.7
9.1.7.1

La couche transport : les protocoles UDP et TCP


Le protocole UDP : User Datagram Protocol

Le protocole UDP permet un change non fiable de donnes entre deux points, c'est dire que le bon acheminement d'un paquet sa destination n'est pas garanti. L'application, si elle le souhaite peut grer cela elle-mme, en attendant par exemple aprs l'envoi d'un message, un accus de rception, avant d'envoyer le suivant. Pour l'instant, au niveau rseau, nous avons parl d'adresses IP de machines. Or sur une machine, peuvent coexister en mme temps diffrents processus qui tous peuvent communiquer. Il faut donc indiquer, lors de l'envoi d'un message, non seulement l'adresse IP de la machine destinatrice, mais galement le "nom" du processus destinataire. Ce nom est en fait un numro, appel numro de port. Certains numros sont rservs des applications standard : port 69 pour l'application tftp (trivial file transfer protocol) par exemple. Les paquets grs par le protocole UDP sont appels galement des datagrammes. Ils ont la forme suivante :
. <---- En-tte datagramme UDP ----------->. <---Donnes datagramme UDP-------->. +--------------------------------------------------------------------------------+ Port source Port destination +--------------------------------------------------------------------------------+

Ces datagrammes seront encapsuls dans des paquets IP, puis dans des trames physiques.

9.1.7.2

Le protocole TCP : Transfer Control Protocol

Pour des communications sres, le protocole UDP est insuffisant : le dveloppeur d'applications doit laborer lui-mme un protocole lui permettant de dtecter le bon acheminement des paquets. Le protocole TCP (Transfer Control Protocol) vite ces problmes. Ses caractristiques sont les suivantes :

Le processus qui souhaite mettre tablit tout d'abord une connexion avec le processus destinataire des informations qu'il va mettre. Cette connexion se fait entre un port de la machine mettrice et un port de la machine rceptrice. Il y a entre les deux ports un chemin virtuel qui est ainsi cr et qui sera rserv aux deux seuls processus ayant ralis la connexion. Tous les paquets mis par le processus source suivent ce chemin virtuel et arrivent dans l'ordre o ils ont t mis ce qui n'tait pas garanti dans le protocole UDP puisque les paquets pouvaient suivre des chemins diffrents. L'information mise a un aspect continu. Le processus metteur envoie des informations son rhythme. Celles-ci ne sont pas ncessairement envoyes tout de suite : le protocole TCP attend d'en avoir assez pour les envoyer. Elles sont stockes dans une structure appele segment TCP. Ce segment une fois rempli sera transmis la couche IP o il sera encapsul dans un paquet IP. Chaque segment envoy par le protocole TCP est numrot. Le protocole TCP destinataire vrifie qu'il reoit bien les segments en squence. Pour chaque segment correctement reu, il envoie un accus de rception l'expditeur. Lorsque ce dernier le reoit, il l'indique au processus metteur. Celui-ci peut donc savoir qu'un segment est arriv bon port, ce qui n'tait pas possible avec le protocole UDP.

Programmation Internet

317

Si au bout d'un certain temps, le protocole TCP ayant mis un segment ne reoit pas d'accus de rception, il retransmet le segment en question, garantissant ainsi la qualit du service d'acheminement de l'information. Le circuit virtuel tabli entre les deux processus qui communiquent est full-duplex : cela signifie que l'information peut transiter dans les deux sens. Ainsi le processus destination peut envoyer des accuss de rception alors mme que le processus source continue d'envoyer des informations. Cela permet par exemple au protocole TCP source d'envoyer plusieurs segments sans attendre d'accus de rception. S'il ralise au bout d'un certain temps qu'il n'a pas reu l'accus de rception d'un certain segment n n, il reprendra l'mission des segments ce point.

9.1.8

La couche Applications

Au-dessus des protocoles UDP et TCP, existent divers protocoles standard : TELNET Ce protocole permet un utilisateur d'une machine A du rseau de se connecter sur une machine B (appele souvent machine hte). TELNET mule sur la machine A un terminal dit universel. L'utilisateur se comporte donc comme s'il disposait d'un terminal connect la machine B. Telnet s'appuie sur le protocole TCP. FTP : (File Transfer protocol) Ce protocole permet l'change de fichiers entre deux machines distantes ainsi que des manipulations de fichiers tels que des crations de rpertoire par exemple. Il s'appuie sur le protocole TCP. TFTP: (Trivial File Transfer Control) Ce protocole est une variante de FTP. Il s'appuie sur le protocole UDP et est moins sophistiqu que FTP. DNS : (Domain Name System) Lorsqu'un utilisateur dsire changer des fichiers avec une machine distante, par FTP par exemple, il doit connatre l'adresse Internet de cette machine. Par exemple, pour faire du FTP sur la machine Lagaffe de l'universit d'Angers, il faudrait lancer FTP comme suit : FTP 193.49.144.1 Cela oblige avoir un annuaire faisant la correspondance machine <--> adresse IP. Probablement que dans cet annuaire les machines seraient dsignes par des noms symboliques tels que : machine DPX2/320 de l'universit d'Angers machine Sun de l'ISERPA d'Angers On voit bien qu'il serait plus agrable de dsigner une machine par un nom plutt que par son adresse IP. Se pose alors le problme de l'unicit du nom : il y a des millions de machines interconnectes. On pourrait imaginer qu'un organisme centralis attribue les noms. Ce serait sans doute assez lourd. Le contrle des noms a t en fait distribu dans des domaines. Chaque domaine est gr par un organisme gnralement trs lger qui a toute libert quant au choix des noms de machines. Ainsi les machines en France appartiennent au domaine fr, domaine gr par l'Inria de Paris. Pour continuer simplifier les choses, on distribue encore le contrle : des domaines sont crs l'intrieur du domaine fr. Ainsi l'universit d'Angers appartient au domaine univ-Angers. Le service grant ce domaine a toute libert pour nommer les machines du rseau de l'Universit d'Angers. Pour l'instant ce domaine n'a pas t subdivis. Mais dans une grande universit comportant beaucoup de machines en rseau, il pourrait l'tre. La machine DPX2/320 de l'universit d'Angers a t nomme Lagaffe alors qu'un PC 486DX50 a t nomm liny. Comment rfrencer ces machines de l'extrieur ? En prcisant la hirarchie des domaines auxquelles elles appartiennent. Ainsi le nom complet de la machine Lagaffe sera : Lagaffe.univ-Angers.fr A l'intrieur des domaines, on peut utiliser des noms relatifs. Ainsi l'intrieur du domaine fr et en dehors du domaine univAngers, la machine Lagaffe pourra tre rfrence par Lagaffe.univ-Angers Enfin, l'intrieur du domaine univ-Angers, elle pourra tre rfrence simplement par Lagaffe Une application peut donc rfrencer une machine par son nom. Au bout du compte, il faut quand mme obtenir l'adresse Internet de cette machine. Comment cela est-il ralis ? Suposons que d'une machine A, on veuille communiquer avec une machine B.

si la machine B appartient au mme domaine que la machine A, on trouvera probablement son adresse IP dans un fichier de la machine A.

Programmation Internet

318

sinon, la machine A trouvera dans un autre fichier ou le mme que prcdemment, une liste de quelques serveurs de noms avec leurs adresses IP. Un serveur de noms est charg de faire la correspondance entre un nom de machine et son adresse IP. La machine A va envoyer une requte spciale au premier serveur de nom de sa liste, appel requte DNS incluant donc le nom de la machine recherche. Si le serveur interrog a ce nom dans ses tablettes, il enverra la machine A, l'adresse IP correspondante. Sinon, le serveur trouvera lui aussi dans ses fichiers, une liste de serveurs de noms qu'il peut interroger. Il le fera alors. Ainsi un certain nombre de serveurs de noms vont tre interrogs, pas de faon anarchique mais d'une faon minimiser les requtes. Si la machine est finalement trouve, la rponse redescendra jusqu' la machine A.

XDR : (eXternal Data Representation) Cr par sun MicroSystems, ce protocole spcifie une reprsentation standard des donnes, indpendante des machines. RPC : (Remote Procedure Call) Dfini galement par sun, c'est un protocole de communication entre applications distantes, indpendant de la couche transport. Ce protocole est important : il dcharge le programmeur de la connaissance des dtails de la couche transport et rend les applications portables. Ce protocole s'appuie sur sur le protocole XDR NFS : Network File System Toujours dfini par Sun, ce protocole permet une machine, de "voir" le systme de fichiers d'une autre machine. Il s'appuie sur le protocole RPC prcdent.

9.1.9

Conclusion

Nous avons prsent dans cette introduction quelques grandes lignes des protocoles Internet. Pour approfondir ce domaine, on pourra lire l'excellent livre de Douglas Comer : Titre Auteur Editeur TCP/IP : Architecture, Protocoles, Applications. Douglas COMER InterEditions

9.2

Les classes .NET de la gestion des adresses IP

Une machine sur le rseau Internet est dfinie de faon unique par une adresse IP (Internet Protocol) qui peut prendre deux formes : IPv4 : code sur 32 bits et reprsente par une chane de la forme "I1.I2.I3.I4" o In est un nombre entre 1 et 254. Ce sont les adresses IP les plus courantes actuellement. IPv6 : code sur 128 bits et reprsente par une chane de la forme "[I1.I2.I3.I4.I5.I6.I7.I8]" o I n est une chane de 4 chiffres hexadcimaux. Dans ce document, nous n'utiliserons pas les adresses IPv6. Une machine peut tre aussi dfinie par un nom galement unique. Ce nom n'est pas obligatoire, les applications utilisant toujours au final les adresses IP des machines. lls sont l pour faciliter la vie des utilisateurs. Ainsi il est plus facile, avec un navigateur, de demander l'URL http://www.ibm.com que l'URL http://129.42.17.99 bien que les deux mthodes soient possibles. Une machine peut avoir plusieurs adresses IP si elle est physiquement connecte plusieurs rseaux en mme temps. Elle a alors une adresse IP sur chaque rseau. Une adresse IP peut tre reprsente de deux faons dans .NET : sous la forme d'une chane de caractres "I1.I2.I3.I4" ou "[I1.I2.I3.I4.I5.I6.I7.I8]" sous la forme d'un objet de type IPAddress La classe IPAddress Parmi les mthodes M, proprits P et constantes C de la classe IPAddress, on trouve les suivantes :
AddressFamily AddressFamily

P famille de l'adresse IP. Le type AddressFamily est une numration. Les deux valeurs courantes sont : AddressFamily.InterNetwork : pour une adresse IPv4 AddressFamily.InterNetworkV6 : pour une adresse IPv6 C l'adresse IP "0.0.0.0". Lorsqu'un service est associ cette adresse, cela signifie qu'il accepte des clients sur toutes les adresses IP de la machine sur laquelle il opre.

IPAddress Any

Programmation Internet

319

IPAddress LoopBack IPAdress None bool TryParse(string ipString, out IPAddress address) bool IsLoopBack string ToString()

C l'adresse IP "127.0.0.1". Appele "adresse de boucle". Lorsqu'un service est associ cette adresse, cela signifie qu'il n'accepte que les clients qui sont sur la mme machine que lui. C l'adresse IP "255.255.255.255". Lorsqu'un service est associ cette adresse, cela signifie qu'il n'accepte aucun client. M essaie de passer l'adresse IP ipString de forme "I1.I2.I3.I4" sous la forme d'un objet IPAddress address. Rend true si l'opration a russi. M rend true si l'adresse IP est "127.0.0.1" M rend l'adrresse IP sous la forme "I1.I2.I3.I4" ou "[I1.I2.I3.I4.I5.I6.I7.I8]"

L'association adresse IP <--> nomMachine est assure par un service distribu de l'internet appel DNS (Domain Name System). Les mthodes statiques de la classe Dns permettent de faire l'association adresse IP <--> nomMachine :
GetHostEntry (string hostNameOrdAddress) GetHostEntry (IPAddress ip) string GetHostName() IPAddress[] GetHostAddresses(string hostNameOrdAddress)

rend une adresse IPHostEntry partir d'une adresse IP sous la forme d'une chane ou partir d'un nom de machine. Lance une exception si la machine ne peut tre trouve. rend une adresse IPHostEntry partir d'une adresse IP de type IPAddress. Lance une exception si la machine ne peut tre trouve. rend le nom de la machine sur laquelle s'excute le programme qui joue cette instruction rend les adresses IP de la machine identifie par son nom ou l'une de ses adresses IP.

Une instance IPHostEntry encapsule les adresses IP, les alias et le nom d'une machine. Le type IPHostEntry est le suivant :
IPAddress[] AddressList String[] Aliases string HostName

P tableau des adresses IP de la machine P les alias DNS de la machine. Ceux-ci sont les noms correspondant aux diffrentes adresses IP de la machine. P le nom d'hte principal de la machine

Considrons le programme suivant qui affiche le nom de la machine sur laquelle il s'excute puis de faon interactive donne les correspondances adresse IP <--> nom Machine :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. using System; using System.Net; namespace Chap9 { class Program { static void Main(string[] args) { // affiche le nom de la machine locale // puis donne interactivement des infos sur les machines rseau // identifies par un nom ou une adresse IP // machine locale Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName()); // question-rponses interactives string machine; IPHostEntry ipHostEntry; while (true) { // saisie du nom ou de l'adresse IP de la machine recherche Console.Write("Machine recherche (rien pour arrter) : "); machine = Console.ReadLine().Trim().ToLower(); // fini ? if (machine == "") return; // gestion exception try { // recherche machine ipHostEntry = Dns.GetHostEntry(machine); // le nom de la machine Console.WriteLine("Machine : " + ipHostEntry.HostName); // les adresses IP de la machine Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]); for (int i = 1; i < ipHostEntry.AddressList.Length; i++) { Console.Write(", {0}" , ipHostEntry.AddressList[i]); } Console.WriteLine(); // les alias de la machine

Programmation Internet

320

36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. } 50. }

if (ipHostEntry.Aliases.Length != 0) { Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]); for (int i = 1; i < ipHostEntry.Aliases.Length; i++) { Console.Write(", {0}" , ipHostEntry.Aliases[i]); } Console.WriteLine(); } } catch { // la machine n'existe pas Console.WriteLine("Impossible de trouver la machine [{0}]",machine); } } }

L'excution donne les rsultats suivants :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. Machine Locale= LISA-AUTO2005A Machine recherche (rien pour arrter) Machine : LISA-AUTO2005A Adresses IP : 127.0.0.1 Machine recherche (rien pour arrter) Machine : LISA-AUTO2005A Adresses IP : 127.0.0.1 Machine recherche (rien pour arrter) Machine : istia.univ-angers.fr Adresses IP : 193.49.146.171 Machine recherche (rien pour arrter) Machine : istia.istia.univ-angers.fr Adresses IP : 193.49.146.171 Machine recherche (rien pour arrter) Impossible de trouver la machine [xx] : localhost : 127.0.0.1 : istia.univ-angers.fr : 193.49.146.171 : xx

9.3
9.3.1

Les bases de la programmation internet


Gnralits

Considrons la communication entre deux machines distantes A et B : Machine A Machine B

Port PA
Rseau

Port PB

Lorsque une application AppA d'une machine A veut communiquer avec une application AppB d'une machine B de l'Internet, elle doit connatre plusieurs choses : l'adresse IP ou le nom de la machine B le numro du port avec lequel travaille l'application AppB. En effet la machine B peut supporter de nombreuses applications qui travaillent sur l'Internet. Lorsqu'elle reoit des informations provenant du rseau, elle doit savoir quelle application sont destines ces informations. Les applications de la machine B ont accs au rseau via des guichets appels galement des ports de communication. Cette information est contenue dans le paquet reu par la machine B afin qu'il soit dlivr la bonne application. les protocoles de communication compris par la machine B. Dans notre tude, nous utiliserons uniquement les protocoles TCP-IP. le protocole de dialogue accept par l'application AppB. En effet, les machines A et B vont se "parler". Ce qu'elles vont dire va tre encapsul dans les protocoles TCP-IP. Nanmoins, lorsqu'au bout de la chane, l'application AppB va recevoir l'information envoye par l'applicaton AppA, il faut qu'elle soit capable de l'interprter. Ceci est analogue la situation o deux personnes A et B communiquent par tlphone : leur dialogue est transport par le tlphone. La parole va tre code sous forme de signaux par le tlphone A, transporte par des lignes tlphoniques, arriver au tlphone B pour y tre dcode. La

Programmation Internet

321

personne B entend alors des paroles. C'est l qu'intervient la notion de protocole de dialogue : si A parle franais et que B ne comprend pas cette langue, A et B ne pourront dialoguer utilement. Aussi les deux applications communicantes doivent -elles tre d'accord sur le type de dialogue qu'elles vont adopter. Par exemple, le dialogue avec un service ftp n'est pas le mme qu'avec un service pop : ces deux services n'acceptent pas les mmes commandes. Elles ont un protocole de dialogue diffrent.

9.3.2

Les caractristiques du protocole TCP

Nous n'tudierons ici que des communications rseau utilisant le protocole de transport TCP. Rappelons ici, les caractristiques de celui-ci :

Le processus qui souhaite mettre tablit tout d'abord une connexion avec le processus destinataire des informations qu'il va mettre. Cette connexion se fait entre un port de la machine mettrice et un port de la machine rceptrice. Il y a entre les deux ports un chemin virtuel qui est ainsi cr et qui sera rserv aux deux seuls processus ayant ralis la connexion. Tous les paquets mis par le processus source suivent ce chemin virtuel et arrivent dans l'ordre o ils ont t mis L'information mise a un aspect continu. Le processus metteur envoie des informations son rythme. Celles-ci ne sont pas ncessairement envoyes tout de suite : le protocole TCP attend d'en avoir assez pour les envoyer. Elles sont stockes dans une structure appele segment TCP. Ce segment une fois rempli sera transmis la couche IP o il sera encapsul dans un paquet IP. Chaque segment envoy par le protocole TCP est numrot. Le protocole TCP destinataire vrifie qu'il reoit bien les segments en squence. Pour chaque segment correctement reu, il envoie un accus de rception l'expditeur. Lorsque ce dernier le reoit, il l'indique au processus metteur. Celui-ci peut donc savoir qu'un segment est arriv bon port. Si au bout d'un certain temps, le protocole TCP ayant mis un segment ne reoit pas d'accus de rception, il retransmet le segment en question, garantissant ainsi la qualit du service d'acheminement de l'information. Le circuit virtuel tabli entre les deux processus qui communiquent est full-duplex : cela signifie que l'information peut transiter dans les deux sens. Ainsi le processus destination peut envoyer des accuss de rception alors mme que le processus source continue d'envoyer des informations. Cela permet par exemple au protocole TCP source d'envoyer plusieurs segments sans attendre d'accus de rception. S'il ralise au bout d'un certain temps qu'il n'a pas reu l'accus de rception d'un certain segment n n, il reprendra l'mission des segments ce point.

9.3.3

La relation client-serveur

Souvent, la communication sur Internet est dissymtrique : la machine A initie une connexion pour demander un service la machine B : il prcise qu'il veut ouvrir une connexion avec le service SB1 de la machine B. Celle-ci accepte ou refuse. Si elle accepte, la machine A peut envoyer ses demandes au service SB1. Celles-ci doivent se conformer au protocole de dialogue compris par le service SB1. Un dialogue demande-rponse s'instaure ainsi entre la machine A qu'on appelle machine cliente et la machine B qu'on appelle machine serveur. L'un des deux partenaires fermera la connexion.

9.3.4

Architecture d'un client

L'architecture d'un programme rseau demandant les services d'une application serveur sera la suivante :
ouvrir la connexion avec le service SB1 de la machine B si russite alors tant que ce n'est pas fini prparer une demande l'mettre vers la machine B attendre et rcuprer la rponse la traiter fin tant que finsi fermer la connexion

9.3.5

Architecture d'un serveur

L'architecture d'un programme offrant des services sera la suivante :


ouvrir le service sur la machine locale tant que le service est ouvert se mettre l'coute des demandes de connexion sur un port dit port d'coute lorsqu'il y a une demande, la faire traiter par une autre tche sur un autre port dit port de service fin tant que

Programmation Internet

322

Le programme serveur traite diffremment la demande de connexion initiale d'un client de ses demandes ultrieures visant obtenir un service. Le programme n'assure pas le service lui-mme. S'il le faisait, pendant la dure du service il ne serait plus l'coute des demandes de connexion et des clients ne seraient alors pas servis. Il procde donc autrement : ds qu'une demande de connexion est reue sur le port d'coute puis accepte, le serveur cre une tche charge de rendre le service demand par le client. Ce service est rendu sur un autre port de la machine serveur appel port de service. On peut ainsi servir plusieurs clients en mme temps. Une tche de service aura la structure suivante :
tant que le service n'a pas t rendu totalement attendre une demande sur le port de service lorsqu'il y en a une, laborer la rponse transmettre la rponse via le port de service fin tant que librer le port de service

9.4
9.4.1

Dcouvrir les protocoles de communication de l'internet


Introduction

Lorsqu'un client s'est connect un serveur, s'tablit ensuite un dialogue entre-eux. La nature de celui-ci forme ce qu'on appelle le protocole de communication du serveur. Parmi les protocoles les plus courants de l'internet on trouve les suivants :

HTTP : HyperText Transfer Protocol - le protocole de dialogue avec un serveur web (serveur HTTP) SMTP : Simple Mail Transfer Protocol - le protocole de dialogue avec un serveur d'envoi de courriers lectroniques (serveur SMTP) POP : Post Office Protocol - le protocole de dialogue avec un serveur de stockage du courrier lectronique (serveur POP). Il s'agit l de rcuprer les courriers lectroniques reus et non d'en envoyer. FTP : File Transfer Protocol - le protocole de dialogue avec un serveur de stockage de fichiers (serveur FTP).

Tous ces protocoles ont la particularit d'tre des protocoles lignes de texte : le client et le serveur s'changent des lignes de texte. Si on a un client capable de : crer une connexion avec un serveur Tcp afficher la console les lignes de texte que le serveur lui envoie envoyer au serveur les lignes de texte qu'un utilisateur saisirait alors on est capable de dialoguer avec un serveur Tcp ayant un protocole lignes de texte pour peu qu'on connaisse les rgles de ce protocole. Le programme telnet qu'on trouve sur les machines Unix ou Windows est un tel client. Sur les machines Windows, on trouve galement un outil appel putty et c'est lui que nous allons utiliser ici. putty est tlchargeable l'adresse [http://www.putty.org/]. C'est un excutable (.exe) directement utilisable. Nous le configurerons de la faon suivante :

Programmation Internet

323

1 3

[1] : l'adresse IP du serveur Tcp auquel on veut se connceter ou son nom [2] : le port d'coute du serveur Tcp [3] : prendre le mode Raw qui dsigne une connexion Tcp brute. [4] : prendre le mode Never pour empcher la fentre du client putty de se fermer si le serveur ferme la connexion. [6,7] : nombre de colonnes / lignes de la console [5] : le nombre maximal de lignes conserves en mmoire. Un serveur HTTP peut envoyer beaucoup de lignes. Il faut pouvoir "scroller" dessus.

8 12 9 11

[8,9] : pour conserver les paramtres prcdents, donner un nom la configuration [8] et la sauvegarder [9]. [11,12] : pour rcuprer une configuration sauvegarde, la slectionner [11] et la charger [12].

Avec cet outil ainsi configur, dcouvrons quelques protocoles TCP.

9.4.2

Le protocole HTTP (HyperText Transfer Protocol)

Connectons [1] notre client TCP sur le serveur web de la machine istia.univ-angers.fr [2], port 80 [3] :

Programmation Internet

324

Dans la console de putty, nous construisons le dialogue HTTP suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. GET / HTTP/1.1 Host: istia.univ-angers.fr:80 Connection: close HTTP/1.1 200 OK Date: Sat, 03 May 2008 07:53:47 GMT Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29 X-Powered-By: PHP/4.4.4-8+etch4 Set-Cookie: fe_typo_user=0d2e64b317; path=/ Connection: close Transfer-Encoding: chunked Content-Type: text/html;charset=iso-8859-1 693f <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR"> .... </html> 0

les lignes 1-4 sont la demande du client, tape au clavier les lignes 5-19 sont la rponse du serveur ligne 1 : syntaxe GET UrlDocument HTTP/1.1 - nous demandons l'Url /, c.a.d. la racine du site web [istia.univ-angers.fr]. ligne 2 : syntaxe Host: machine:port ligne 3 : syntaxe Connection: [mode de la connexion]. Le mode [close] indique au serveur de fermer la connexion une fois qu'il aura envoye sa rponse. Le mode [Keep-Alive] demande de la laisser ouverte. ligne 4 : ligne vide. Les lignes 1-3 sont appeles enttes HTTP. Il peut y en avoir d'autres que ceux prsents ici. La fin des enttes HTTP est signale avec une ligne vide. lignes 5-13 : les enttes HTTP de la rponse du serveur - se terminent l galement par une ligne vide. lignes 14-19 : le document envoy par le serveur, ici un document HTML ligne 5 : syntaxe HTTP/1.1 code msg - le code 200 indique que le document demand a t trouv. ligne 6 : les date et heure du serveur ligne 7 : identification logiciel assurant le service web - ici un serveur Apache sur un Linux / Debian ligne 8 : le document a t gnr dynamiquement par PHP ligne 9 : cookie d'identification du client - si celui-ci veut se faire reconnatre sa prochaine connexion, il devra renvoyer ce cookie dans ses enttes HTTP. ligne 10 : indique qu'aprs avoir servi le document demand, le serveur fermera la connexion ligne 11 : le document va tre transmis par morceaux (chunked) et non d'un seul bloc. ligne 12 : nature du document : ici un document HTML ligne 13 : la ligne vide qui signale la fin des enttes HTTP du serveur ligne 14 : nombre hexadcimal indiquant le nombre de caractres du 1er bloc du document. Lorsque ce nombre vaudra 0 (ligne 19), le client saura qu'il a reu tout le document. lignes 15-18 : partie du document reu.

Programmation Internet

325

La connexion a t ferme et le client putty est inactif. Reconnectons-nous [1] et nettoyons l'cran des affichages prcdents [2,3] :

Le dialogue cette fois-ci est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. GET /inconnu HTTP/1.1 Host: istia.univ-angers.fr:80 Connection: Close HTTP/1.1 404 Not Found Date: Sat, 03 May 2008 08:16:02 GMT Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29 Connection: close Transfer-Encoding: chunked Content-Type: text/html; charset=iso-8859-1 11a <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">

<HTML><HEAD> <TITLE>404 Not Found</TITLE> </HEAD><

BODY> <H1>Not Found</H1> The requested URL /inconnu was not found on this server.<P> <HR> <ADDRESS>Apache/1.3.34 Server at

www.istia.univ-angers.fr Port 80</ADDRESS> 21. </BODY></HTML> 22. 23. 0

ligne 1 : on a demand un document inexistant ligne 5 : le serveur HTTP a rpondu avec le code 404 signifiant que le document demand n'a pas t trouv.

Si on demande ce document avec un navigateur Firefox :

Programmation Internet

326

Si nous demandons voir le code source [Affichage/Code source] :


1. 2. 3. 4. 5. 6. 7. 8. 9. <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <HTML><HEAD> <TITLE>404 Not Found</TITLE> </HEAD><BODY> <H1>Not Found</H1> The requested URL /inconnu was not found on this server.<P> <HR> <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS> </BODY></HTML>

Nous obtenons les lignes 13-22 reues par notre client putty. L'intrt de celui-ci est de nous montrer en plus, les enttes HTTP de la rponse. Il est galement possible d'avoir ceux-ci avec Firefox.

9.4.3

Le protocole SMTP (Simple Mail Transfer Protocol)

2 1

Les serveurs SMTP oprent en gnral sur le port 25 [2]. On se connecte sur le serveur [1]. Ici, il faut en gnral prendre un serveur appartenant au mme domaine IP que la machine car le plus souvent les serveurs SMTP sont configurs pour n'accepter que les demandes des machines appartenant au mme domaine qu'eux. Par ailleurs, assez souvent galement, les pare-feu ou antivirus des machines personnelles sont configurs pour ne pas accepter de connexion vers le port 25 d'une machine extrieure. Il peut tre alors ncessaire de reconfigurer [3] ce pare-feu ou antivirus. Le dialogue SMTP dan la fentre du client putty est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 220 neuf-infra-smtp-out-sp604001av.neufgp.fr neuf telecom Service relais mail ready HELO istia.univ-angers.fr 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail mail from: @expditeur 250 2.1.0 <@expditeur> sender ok rcpt to: @destinataire 250 2.1.5 <@destinataire> destinataire ok data 354 enter mail, end with "." on a line by itself ligne1 ligne2 . 250 2.0.0 LwiU1Z00V4AoCxw0200000 message ok quit 221 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection

Ci-dessous (D) est une demande du client, (R) une rponse du serveur. ligne 1 : (R) message d'accueil du serveur SMTP ligne 2 : (D) commande HELO pour dire bonjour ligne 3 : (R) rponse du serveur ligne 4 : (D) adresse expditeur, par exemple mail from: someone@gmail.com ligne 5 : (R) rponse du serveur ligne 6 : (D) adresse destinataire, par exemple rcpt to: someoneelse@gmail.com

Programmation Internet

327

ligne 7 : (R) rponse du serveur ligne 8 : (D) signale le dbut du message ligne 9 : (R) rponse du serveur lignes 10-12 : (D) le message envoyer termin par une ligne contenant uniquement un point. ligne 13 : (R) rponse du serveur ligne 14 : (D) le client signale qu'il a termin ligne 15 : (R) rponse du serveur qui ensuite ferme la connexion

9.4.4

Le protocole POP (Post Office Protocol)

2 1

Les serveurs POP oprent en gnral sur le port 110 [2]. On se connecte sur le serveur [1]. Le dialogue POP dans la fentre du client putty est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. +OK Hello there. user xx +OK Password required. pass yy +OK logged in. list +OK POP3 clients that break here, they violate STD53. 1 10105 2 55875 ... 64 1717 . retr 64 +OK 1717 octets follow. Return-Path: <xx@neuf.fr> X-Original-To: xx@univ-angers.fr Delivered-To: xx@univ-angers.fr .... Date: Sat, 3 May 2008 10:59:25 +0200 (CEST) From: xx@neuf.fr To: undisclosed-recipients:; ligne1 ligne2 . quit +OK Bye-bye.

ligne 1 : (R) message de bienvenue du serveur POP ligne 2 : (D) le client donne son identifiant POP, c.a.d. le login avec lequel il lit son courrier ligne 3 : (R) la rponse du serveur ligne 4 : (D) le mot de passe du client ligne 5 : (R) la rponse du serveur ligne 6 : (D) le client demande la liste de ses courriers lignes 7-12 : (R) la liste des messages dans la bote lettre du client, sous la forme [N du message taille en octets du message] ligne 13 : (D) on demande le message n 64 lignes 14-25 : (R) le message n 64 avec lignes 15-22, les enttes du message, et lignes 23-24 le corps du message. ligne 26 : (D) le client indique qu'il a fini ligne 27 : (R) rponse du serveur qui va ensuite fermer la connexion.

9.4.5

Le protocole FTP (File Transfer Protocol)

Programmation Internet

328

Le protocole FTP est plus complexe que ceux prsents prcdemment. Pour dcouvrir les lignes de texte changes entre le client et le serveur, on pourra utiliser un outil tel que FileZilla [http://www.filezilla.fr/].

Filezilla est un client FTP offrant une interface windows pour faire des transferts de fichiers. Les actions de l'utilisateur sur l'interface windows sont traduites en commandes FTP qui sont logues en [1]. C'est une bonne faon de dcouvrir les commandes du protocole FTP.

9.5
9.5.1

Les classes .NET de la programmation internet


Choisir la classe adapte

Le framework .NET offre diffrentes classes pour travailler avec le rseau : Application C Application D

WebClient
Application A

SmtpClient Socket

Application B

TcpClient, TcpListener
Rseau

la classe Socket est celle qui opre le plus prs du rseau. Elle permet de grer finement la connexion rseau. Le terme socket dsigne une prise de courant. Le terme a t tendu pour dsigner une prise de rseau logicielle. Dans une communication TCP-IP entre deux machines A et B, ce sont deux sockets qui communiquent entre-eux. Une application peut travailler directement avec les sockets. C'est le cas de l'application A ci-dessus. Un socket peut tre un socket client ou serveur. si on souhaite travailler un niveau moins fin que celui de la classe Socket, on pourra utiliser les classes TcpClient pour crer un client Tcp TcpListener pour crer un serveur Tcp Ces deux classes offrent l'application qui les utilisent, une vue plus simple de la communication rseau en grant pour elle les dtails techniques de gestion des sockets. .NET offre des classes spcifiques certains protocoles : la classe SmtpClient pour grer le protocole SMTP de communication avec un serveur SMTP d'envoi de courriers lectroniques la classe WebClient pour grer les protocoles HTTP ou FTP de communication avec un serveur web.

On retiendra que la classe Socket est suffisante en elle-mme pour grer toute communication tcp-ip mais on cherchera avant tout utiliser les classes de plus haut niveau afin de faciliter l'criture de l'application tcp-ip.

9.5.2

La classe TcpClient

Programmation Internet

329

La classe TcpClient est la classe qui convient dans la plupart des cas pour crer le client d'un service TCP. Elle a parmi ses constructeurs C, mthodes M et proprits P, les suivants :
TcpClient(string hostname, int port)

C cre une liaison tcp avec le service oprant sur le port indiqu (port) de la machine indique (hostname). Par exemple new TcpClient("istia.univ-angers.fr",80) pour se connecter au port 80 de la machine istia.univ-angers.fr P le socket utilis par le client pour communiquer avec le serveur. M obtient un flux de lecture et d'criture vers le serveur. C'est ce flux qui permet les changes client-serveur. M ferme la connexion. Le socket et le flux NetworkStream sont galement ferms P vrai si la connexion a t tablie

Socket Client NetworkStream GetStream() void Close() bool Connected()

La classe NetworkStream reprsente le flux rseau entre le client et le serveur. Elle est drive de la classe Stream. Beaucoup d'applications client-serveur changent des lignes de texte termines par les caractres de fin de ligne "\r\n". Aussi est-il intressant d'utiliser des objets StreamReader et StreamWriter pour lire et crire ces lignes dans le flux rseau. Ainsi si une machine M1 a tabli une liaison avec une machine M2 l'aide d'un objet TcpClient client1 et qu'elles changent des lignes de texte, elle pourra crer ses flux de lecture et criture de la faon suivante :
StreamReader in1=new StreamReader(client1.GetStream()); StreamWriter out1=new StreamWriter(client1.GetStream()); out1.AutoFlush=true;

L'instruction
out1.AutoFlush=true;

signifie que le flux d'criture de client1 ne transitera pas par un buffer intermdiaire mais ira directement sur le rseau. Ce point est important. En gnral lorsque client1 envoie une ligne de texte son partenaire il en attend une rponse. Celle-ci ne viendra jamais si la ligne a t en ralit bufferise sur la machine M1 et jamais envoye la machine M2. Pour envoyer une ligne de texte la machine M2, on crira :
client1.WriteLine("un texte");

Pour lire la rponse de M2, on crira :


string rponse=client1.ReadLine();

Nous avons maintenant les lments pour crire l'architecture de base d'un client internet ayant le protocole de communication basique suivant avec le serveur : le client envoie une demande contenue dans une unique ligne le serveur envoie une rponse contenue dans une unique ligne
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. using System; using System.IO; using System.Net.Sockets; namespace ... { class ... { static void Main(string[] args) { ... try { // on se connecte au service using (TcpClient tcpClient = new TcpClient(serveur, port)) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamReader reader = new StreamReader(networkStream)) { using (StreamWriter writer = new StreamWriter(networkStream)) { // flux de sortie non bufferis writer.AutoFlush = true; // boucle demande - rponse while (true) { // la demande vient du clavier Console.Write("Demande (bye pour arrter) : "); demande = Console.ReadLine(); // fini ? if (demande.Trim().ToLower() == "bye")

Programmation Internet

330

24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. } 42. }

break; // on envoie la demande au serveur writer.WriteLine(demande); // on lit la rponse du serveur rponse = reader.ReadLine(); // on traite la rponse ... } } } }

} } catch (Exception e) { // erreur ... }

ligne 11 : cration connexion du client - la clause using assure que les ressources lies celle-ci seront libres la sortie du using. ligne 12 : ouverture du flux rseau dans une clause using ligne 13 : cration et exploitation du flux de lecture dans une clause using ligne 14 : cration et exploitation du flux d'criture dans une clause using ligne 16 : ne pas bufferiser le flux de sortie lignes 18-31 : le cycle demande client / rponse serveur ligne 26 : le client envoie sa demande au serveur ligne 28 : le client attend la rponse du serveur. C'est une opration bloquante comme celle de la lecture au clavier. L'attente se termine par l'arrive d'une chane termine par "\n" ou bien par une fin de flux. Celle-ci se produira si le serveur ferme la connexion qu'il a ouverte avec le client.

9.5.3

La classe TcpListener

La classe TcpListener est la classe qui convient dans la plupart des cas pour crer un service TCP. Elle a parmi ses constructeurs C, mthodes M et proprits P, les suivants :
TcpListener(int port)

C cre un service TCP qui va attendre (listen) les demandes des clients sur un port pass en paramtre (port) appel port d'coute. Si la machine est connecte plusieurs rseaux IP, le service coute sur chacun des rseaux. C idem mais l'coute n'a lieu que sur l'adresse ip prcise. M lance l'coute des demandes clients M accepte la demande d'un client. Ouvre alors une nouvelle connexion avec celui-ci, appele connexion de service. Le port utilis ct serveur est alatoire et choisi par le systme. On l'appelle le port de service. AcceptTcpClient rend comme rsultat l'objet TcpClient associ ct serveur la connexion de service. M arrte d'couter les demandes clients P le socket d'coute du serveur

TcpListener(IPAddress ip, int port) void Start() TcpClient AcceptTcpClient()

void Stop() Socket Server

La structure de base d'un serveur TCP qui changerait avec ses clients selon le protocole suivant : le client envoie une demande contenue dans une unique ligne le serveur envoie une rponse contenue dans une unique ligne pourrait ressembler ceci :
1. 2. 3. 4. 5. 6. 7. 8. using using using using using System; System.IO; System.Net.Sockets; System.Threading; System.Net;

namespace ... { public class ... {

Programmation Internet

331

9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. } 73. }

... // on cre le service d'coute TcpListener ecoute = null; try { // on cre le service - il coutera sur toutes les interfaces rseau de la machine ecoute = new TcpListener(IPAddress.Any, port); // on le lance ecoute.Start(); // boucle de service TcpClient tcpClient = null; // boucle infinie - sera arrte par Ctrl-C while (true) { // attente d'un client tcpClient = ecoute.AcceptTcpClient(); // le service est assur par une autre tche ThreadPool.QueueUserWorkItem(Service, tcpClient); // client suivant } } catch (Exception ex) { // on signale l'erreur ... } finally { // fin du service ecoute.Stop(); } } // ------------------------------------------------------// assure le service un client public static void Service(Object infos) { // on rcupre le client qu'il faut servir Client client = infos as Client; // exploitation liaison TcpClient try { using (TcpClient tcpClient = client.CanalTcp) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamReader reader = new StreamReader(networkStream)) { using (StreamWriter writer = new StreamWriter(networkStream)) { // flux de sortie non bufferis writer.AutoFlush = true; // boucle lecture demande/criture rponse bool fini=false; while (! fini) != null) { // attente demande client - opration bloquante demande=reader.ReadLine(); // prparation rponse rponse=...; // envoi rponse au client writer.WriteLine(rponse); // demande suivante } } } } } } catch (Exception e) { // erreur ... } finally { // fin client ... } }

ligne 14 : le service d'coute est cr pour un port donn et une adresse IP donne. Il faut se rappeler ici qu'une machine a au moins deux adresses IP : l'adresse "127.0.0.1" qui est son adresse de bouclage sur elle-mme et l'adresse "I1.I2.I3.I4" qu'elle a sur le rseau auquel elle est connecte. Elle peut avoir d'autres adresses IP si elle connecte plusieurs rseaux IP. IPAddress.Any dsigne toutes les adresses IP d'une machine. ligne 16 : le service d'coute dmarre. Auparavant il avait t cr mais il n'coutait pas encore. Ecouter signifie attendre les demandes des clients. lignes 20-26 : la boucle attente demande client / service client rpte pour chaque nouveau client ligne 22 : la demande d'un client est accepte. La mthode AcceptTcpClient rend une instance TcpClient dite de service : le client a fait sa demande avec sa propre instance TcpClient ct client que nous appellerons TcpClientDemande

Programmation Internet

332

le serveur accepte cette demande avec AcceptTcpClient. Cette mthode cre une instance TcpClient ct serveur, que nous appellerons TcpClientService. On a alors une connexion Tcp ouverte avec aux deux bouts les instances TcpClientDemande <--> TcpClientService. la communication client / serveur qui prend place ensuite se fait sur cette connexion. Le service d'coute n'intervient plus. ligne 24 : afin que le serveur puisse traiter plusieurs clients la fois, le service est assur par des threads, 1 thread par client. ligne 32 : le service d'coute est ferm ligne 38 : la mthode excute par le thread de service un client. Elle reoit en paramtre l'instance TcpClient dj connecte au client qui doit tre servi. lignes 38-71 : on retrouve un code similaire celui du client Tcp basique tudi prcdemment.

9.6
9.6.1

Exemples de clients / serveurs TCP


Un serveur d'cho

Nous nous proposons d'crire un serveur d'cho qui sera lanc depuis une fentre DOS par la commande : ServeurEcho port Le serveur officie sur le port pass en paramtre. Il se contente de renvoyer au client la demande que celui-ci lui a envoye. Le programme est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. using using using using using System; System.IO; System.Net.Sockets; System.Threading; System.Net;

// appel : serveurEcho port // serveur d'cho // renvoie au client la ligne que celui-ci lui a envoye namespace Chap9 { public class ServeurEcho { public const string syntaxe = "Syntaxe : [serveurEcho] port"; // programme principal public static void Main(string[] args) { // y-a-t-il un argument ? if (args.Length != 1) { Console.WriteLine(syntaxe); return; } // cet argument doit tre entier >0 int port = 0; if (!int.TryParse(args[0], out port) || port<=0) { Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine); return; } // on cre le service d'coute TcpListener ecoute = null; int numClient = 0; // n client suivant try { // on cre le service - il coutera sur toutes les interfaces rseau de la machine ecoute = new TcpListener(IPAddress.Any, port); // on le lance ecoute.Start(); // suivi Console.WriteLine("Serveur d'cho lanc sur le port {0}", ecoute.LocalEndpoint); // threads de service ThreadPool.SetMinThreads(10, 10); ThreadPool.SetMaxThreads(10, 10); // boucle de service TcpClient tcpClient = null; // boucle infinie - sera arrte par Ctrl-C while (true) { // attente d'un client tcpClient = ecoute.AcceptTcpClient(); // le service est assur par une autre tche

Programmation Internet

333

49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96.

ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient }); // client suivant numClient++; } } catch (Exception ex) { // on signale l'erreur Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message); } finally { // fin du service ecoute.Stop(); } }

// ------------------------------------------------------// assure le service un client du serveur d'cho public static void Service(Object infos) { // on rcupre le client qu'il faut servir Client client = infos as Client; // rend le service au client Console.WriteLine("Dbut de service au client {0}", client.NumClient); // exploitation liaison TcpClient try { using (TcpClient tcpClient = client.CanalTcp) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamReader reader = new StreamReader(networkStream)) { using (StreamWriter writer = new StreamWriter(networkStream)) { // flux de sortie non bufferis writer.AutoFlush = true; // boucle lecture demande/criture rponse string demande = null; while ((demande = reader.ReadLine()) != null) { // suivi console Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande); // cho de la demande vers le client writer.WriteLine("[{0}]", demande); // suivi console Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande); // le service s'arrte lorsque le client envoie "bye" if (demande.Trim().ToLower() == "bye") break; } } } } } } catch (Exception e) { // erreur Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message); 97. } finally { 98. // fin client 99. Console.WriteLine("Fin du service au client {0}", client.NumClient); 100. } 101. } 102. } 103. 104. // infos client 105. internal class Client { 106. public TcpClient CanalTcp { get; set; } // liaison avec le client 107. public int NumClient { get; set; } // n de client 108. } 109.}

La structure du serveur d'cho est conforme l'architecture basique des serveurs Tcp expose prcdemment. Nous ne commenterons que la partie "service au client" :

ligne 79 : la demande du client est lue ligne 83 : elle est renvoye au client entoure de crochets ligne 79 : le service s'arrte lorsque le client ferme la connexion

Dans une fentre Dos, nous utilisons l'excutable du projet C# :


...\Chap9\02\bin\Release>dir 03/05/2008 11:46 7 168 ServeurEcho.exe

Programmation Internet

334

...>ServeurEcho 100 Serveur d'cho lanc sur le port 0.0.0.0:100

Nous lanons ensuite deux clients putty que nous connectons au port 100 de la machine localhost :

L'affichage console du serveur d'cho devient :


1. 2. 3. Serveur d'cho lanc sur le port 0.0.0.0:100 Dbut de service au client 0 Dbut de service au client 1

Le client 1 puis le client 0 envoient les textes suivants : 3

[1] : le client n 1 [2] : le client n 0 [3] : la console du serveur d'cho

5 6

en [4] : le client 1 se dconnecte avec la commande bye. en [5] : le serveur le dtecte

Le serveur peut tre arrt par Ctrl-C. Le client n 0 le dtecte alors [6].

9.6.2

Un client pour le serveur d'cho

Nous crivons maintenant un client pour le serveur prcdent. Il sera appel de la faon suivante : ClientEcho nomServeur port

Programmation Internet

335

Il se connecte la machine nomServeur sur le port port puis envoie au serveur des lignes de texte que celui-ci lui renvoie en cho.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. using System; using System.IO; using System.Net.Sockets; namespace Chap9 { // se connecte un serveur d'cho // toute ligne tape au clavier est reue en cho class ClientEcho { static void Main(string[] args) { // syntaxe const string syntaxe = "pg machine port"; // nombre d'arguments if (args.Length != 2) { Console.WriteLine(syntaxe); return; } // on note le nom du serveur string serveur = args[0]; // le port doit tre entier >0 int port = 0; if (!int.TryParse(args[1], out port) || port <= 0) { Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine); return; } // on peut travailler string demande = null; // demande du client string rponse = null; // rponse du serveur try { // on se connecte au service using (TcpClient tcpClient = new TcpClient(serveur, port)) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamReader reader = new StreamReader(networkStream)) { using (StreamWriter writer = new StreamWriter(networkStream)) { // flux de sortie non bufferis writer.AutoFlush = true; // boucle demande - rponse while (true) { // la demande vient du clavier Console.Write("Demande (bye pour arrter) : "); demande = Console.ReadLine(); // fini ? if (demande.Trim().ToLower() == "bye") break; // on envoie la demande au serveur writer.WriteLine(demande); // on lit la rponse du serveur rponse = reader.ReadLine(); // on traite la rponse Console.WriteLine("Rponse : {0}", rponse); } } } } } } catch (Exception e) { // erreur Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message); }

} } }

La structure de ce client est conforme l'architecture gnrale basique propose pour les clients Tcp. Voici les rsultats obtenus dans la configuration suivante : le serveur est lanc sur le port 100 dans une fentre Dos sur la mme machine deux clients sont lancs dans deux autres fentres Dos Dans la fentre du client A (n 0) on a les affichages suivants :
1. ...\Chap9\03\bin\Release>ClientEcho localhost 100

Programmation Internet

336

2. 3. 4. 5. 6.

Demande Rponse Demande Rponse Demande

(bye pour arrter) : ligne1A : [ligne1A] (bye pour arrter) : ligne2A : [ligne2A] (bye pour arrter) :

Dans celle du client B (n 1) :


1. 2. 3. 4. 5. 6. ...\Chap9\03\bin\Release>ClientEcho localhost 100 Demande (bye pour arrter) : ligne1B Rponse : [ligne1B] Demande (bye pour arrter) : ligne2B Rponse : [ligne2B] Demande (bye pour arrter) :

Dans celle du serveur :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. ...\Chap9\02\bin\Release>ServeurEcho 100 Serveur d'cho lanc sur le port 0.0.0.0:100 Dbut de service au client 0 <--- Client 0 : ligne1A ---> Client 0 : ligne1A <--- Client 0 : ligne2A ---> Client 0 : ligne2A Dbut de service au client 1 <--- Client 1 : ligne1B ---> Client 1 : ligne1B <--- Client 1 : ligne2B ---> Client 1 : ligne2B

Le client A n 0 se dconnecte :
1. 2. 3. 4. Demande (bye pour arrter) : ligne1A Rponse : [ligne1A] ... Demande (bye pour arrter) : bye

La console du serveur :
1. 2. 3. Serveur d'cho lanc sur le port 0.0.0.0:100 ... Fin du service au client 0

9.6.3

Un client TCP gnrique

Nous allons crire un client Tcp gnrique qui sera lanc de la faon suivante : ClientTcpGenerique serveur port. Il aura un fonctionnement analogue au client putty mais aura une interface console et ne prsentera pas d'option de configuration. Dans l'application prcdente, le protocole du dialogue tait connu : le client envoyait une seule ligne et le serveur rpondait par une seule ligne. Chaque service a son protocole particulier et on trouve galement les situations suivantes : le client doit envoyer plusieurs lignes de texte avant d'avoir une rponse la rponse d'un serveur peut comporter plusieurs lignes de texte Aussi le cycle envoi d'une unique ligne au serveur / rception d'une unique ligne envoye par le serveur, ne convient-il pas toujours. Pour grer les protocoles plus complexes que celui d'cho, le client Tcp gnrique aura deux threads : le thread principal lira les lignes de texte tapes au clavier et les enverra au serveur. un thread secondaire travaillera en parallle et sera consacr la lecture des lignes de texte envoyes par le serveur. Ds qu'il en reoit une, il l'affiche sur la console. Le thread ne s'arrte que lorsque le serveur clt la connexion. Il travaille donc en continu. Le code est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. using using using using System; System.IO; System.Net.Sockets; System.Threading;

namespace Chap9 { // reoit en paramtre les caractristiques d'un service sous la forme : serveur port // se connecte au service

Programmation Internet

337

9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37.

// envoie au serveur chaque ligne tape au clavier // cre un thread pour lire en continu les lignes de texte envoyes par le serveur class ClientTcpGenerique { static void Main(string[] args) { // syntaxe const string syntaxe = "pg serveur port"; // nombre d'arguments if (args.Length != 2) { Console.WriteLine(syntaxe); return; } // on note le nom du serveur string serveur = args[0];

// le port doit tre entier >0 int port = 0; if (!int.TryParse(args[1], out port) || port <= 0) { Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine); return; } // on se connecte au service TcpClient tcpClient = null; try { tcpClient = new TcpClient(serveur, port); } catch (Exception ex) { // erreur Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message); 38. // fin 39. return; 40. } 41. 42. // on lance un thread part pour lire les lignes de texte envoyes par le serveur 43. ThreadPool.QueueUserWorkItem(Receive, tcpClient); 44. 45. // la lecture des commandes clavier se fait dans le thread principal 46. Console.WriteLine("Tapez vos commandes (bye pour arrter) : "); 47. string demande = null; // demande du client 48. try { 49. // on exploite la connexion client 50. using (tcpClient) { 51. // on cre un flux d'criture vers le serveur 52. using (NetworkStream networkStream = tcpClient.GetStream()) { 53. using (StreamWriter writer = new StreamWriter(networkStream)) { 54. // flux de sortie non bufferis 55. writer.AutoFlush = true; 56. // boucle demande - rponse 57. while (true) { 58. demande = Console.ReadLine(); 59. // fini ? 60. if (demande.Trim().ToLower() == "bye") 61. break; 62. // on envoie la demande au serveur 63. writer.WriteLine(demande); 64. } 65. } 66. } 67. } 68. } catch (Exception e) { 69. // erreur 70. Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message); 71. } 72. } 73. 74. // thread de lecture client <-- serveur 75. public static void Receive(object infos) { 76. // donnes locales 77. string rponse = null; // rponse du serveur 78. // cration flux d'entre 79. try { 80. using (TcpClient tcpClient = infos as TcpClient) { 81. using (NetworkStream networkStream = tcpClient.GetStream()) { 82. using (StreamReader reader = new StreamReader(networkStream)) { 83. // boucle lecture en continu des lignes de texte du flux d'entre 84. while ((rponse = reader.ReadLine()) != null) { 85. // affichage console 86. Console.WriteLine("<-- {0}", rponse); 87. } 88. } 89. } 90. } 91. } catch (Exception ex) { 92. // erreur 93. Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message); 94. } finally { 95. // on signale la fin du thread de lecture

Programmation Internet

338

96.

Console.WriteLine("Fin du thread de lecture des rponses du serveur. Si besoin est, arrtez le thread de lecture console avec la commande bye."); 97. } 98. } 99. } 100. }

ligne 34 : le client se connecte au serveur ligne 43 : un thread de lecture des lignes de texte du serveur est lanc. Il doit excuter la mthode Receive de la ligne 73. On passe cette mthode l'instance TcpClient qui a t connecte au serveur. lignes 57-64 : la boucle saisie commande clavier / envoi commande au serveur. La saisie des commandes clavier est assure par le thread principal. lignes 75-98 : la mthode Receive excute par le thread de lecture des lignes de texte. Cette mthode reoit en paramtre l'instance TcpClient qui a t connecte au serveur. lignes 84-87 : la boucle en continu de lecture des lignes de texte envoyes par le serveur. Elle ne s'arrte que lorsque le serveur clt la connexion ouverte avec le client.

Voici quelques exemples reprenant ceux utiliss avec le client putty au paragraphe 9.4, page 323. Le client est excut dans une console Dos. Protocole HTTP
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. ...\Chap9\04\bin\Release>ClientTcpGenerique istia.univ-angers.fr 80 Tapez vos commandes (bye pour arrter) : GET /inconnu HTTP/1.1 Host: istia.univ-angers.fr:80 Connection: Close <-- HTTP/1.1 404 Not Found <-- Date: Sat, 03 May 2008 12:35:11 GMT <-- Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29 <-- Connection: close <-- Transfer-Encoding: chunked <-- Content-Type: text/html; charset=iso-8859-1 <-<-- 11a <-- <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <-- <HTML><HEAD> <-- <TITLE>404 Not Found</TITLE> <-- </HEAD><BODY> <-- <H1>Not Found</H1> <-- The requested URL /inconnu was not found on this server.<P> <-- <HR> <-- <ADDRESS>Apache/1.3.34 Server at www.istia.univ-angers.fr Port 80</ADDRESS> <-- </BODY></HTML> <-<-- 0 <-[Fin du thread de lecture des rponses du serveur] bye ...\Chap9\04\bin\Release>

Le lecteur est invit relire les explications donnes au paragraphe 9.4.2, page 325. Nous ne commentons que ce qui est propre l'application :

ligne 28 : aprs l'envoi de la ligne 27, le serveur HTTP a ferm la connexion, ce qui a provoqu la fin du thread de lecture. Le thread principal qui lit les commandes tapes au clavier est lui toujours actif. La commande de la ligne 29, tape au clavier, l'arrte.

Protocole SMTP
1. 2. 3. 4. 5. 6. 7. 8. 9. ...\Chap9\04\bin\Release>ClientTcpGenerique smtp.neuf.fr 25 Tapez vos commandes (bye pour arrter) : <-- 220 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom Service relais mail ready HELO istia.univ-angers.fr <-- 250 neuf-infra-smtp-out-sp604002av.neufgp.fr hello [84.100.189.193], Banniere OK , pret pour envoyer un mail mail from: xx@neuf.fr <-- 250 2.1.0 <xx@neuf.fr> sender ok rcpt to: yy@univ-angers.fr <-- 250 2.1.5 <yy@univ-angers.fr> destinataire ok

Programmation Internet

339

10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21.

data <-- 354 ligne1 ligne2 . <-- 250 quit <-- 221 [Fin du bye

enter mail, end with "." on a line by itself

2.0.0 M0jL1Z0044AoCxw0200000 message ok 2.0.0 neuf-infra-smtp-out-sp604002av.neufgp.fr neuf telecom closing connection thread de lecture des rponses du serveur]

...\Chap9\04\bin\Release>

Le lecteur est invit relire les explications donnes au paragraphe 9.4.3, page 327 et tester les autres exemples utiliss avec le client putty.

9.6.4

Un serveur Tcp gnrique

Maintenant nous nous intressons un serveur qui affiche l'cran les commandes envoyes par ses clients leur envoie comme rponse les lignes de texte tapes au clavier par un utilisateur. C'est donc ce dernier qui fait office de serveur. Le programme est lanc dans une fentre Dos par : ServeurTcpGenerique portEcoute, o portEcoute est le port sur lequel les clients doivent se connecter. Le service au client sera assur par deux threads : le thread principal qui : traitera les clients les uns aprs les autres et non en parallle. qui lira les lignes tapes au clavier par l'utilisateur et les enverra au client. L'utilisateur signalera par la commande bye qu'il clt la connexion avec le client. C'est parce que la console ne peut tre utilise pour deux clients simultanment que notre serveur ne traite qu'un client la fois. un thread secondaire se consacrant exclusivement la lecture des lignes de texte envoyes par le client Le serveur lui ne s'arrte jamais sauf par un Ctrl-C tap au clavier par l'utilisateur. Voyons quelques exemples. Le serveur est lanc sur le port 100 et on utilise le client gnrique du paragraphe9.6.3, page 337, pour lui parler. La fentre du client est la suivante :
1. 2. 3. 4. 5. 6. 7. ...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100 Tapez vos commandes (bye pour arrter) : commande 1 du client 1 <-- rponse 1 au client 1 commande 2 du client 1 <-- rponse 2 au client 1 bye

Les lignes commenant par <-- sont celles envoyes du serveur au client, les autres celles du client vers le serveur. La fentre du serveur est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. ...\Chap9\05\bin\Release>ServeurTcpGenerique 100 Serveur gnrique lanc sur le port 0.0.0.0:100 Client 127.0.0.1:4165 Tapez vos commandes (bye pour arrter) : <-- commande 1 du client 1 rponse 1 au client 1 <-- commande 2 du client 1 rponse 2 au client 1 [Fin du thread de lecture des demandes du client] bye

Les lignes commenant par <-- sont celles envoyes du client au serveur, les autres celles envoyes par le serveur au client. La ligne 9 indique que le thread de lecture des demandes du client s'est arrt. Le thread principal du serveur est toujours en attente de commandes tapes au clavier pour les envoyer au client. Il faut alors taper au clavier la commande bye de la ligne 10 pour passer au client suivant. Le serveur est encore actif alors que le client 1 est termin. On lance un second client pour le mme serveur :
1. 2. 3. 4. 5. ...\Chap9\04\bin\Release>ClientTcpGenerique localhost 100 Tapez vos commandes (bye pour arrter) : commande 3 du client 2 <-- rponse 3 au client 2 bye

Programmation Internet

340

La fentre du serveur est alors celle-ci :


1. 2. 3. 4. 5. 6. Tapez vos commandes (bye pour arrter) : Client 127.0.0.1:4166 <-- commande 3 du client 2 rponse 3 au client 2 [Fin du thread de lecture des demandes du client] bye

Aprs la ligne 6 ci-dessus, le serveur est pass en attente d'un nouveau client. On peut l'arrter par Ctrl-C. Simulons maintenant un serveur web en lanant notre serveur gnrique sur le port 88 :
1. 2. 3. ...\Chap9\05\bin\Release>ServeurTcpGenerique 88 Serveur gnrique lanc sur le port 0.0.0.0:88

Prenons maintenant un navigateur et demandons l'URL http://localhost:88/exemple.html. Le navigateur va alors se connecter sur le port 88 de la machine localhost puis demander la page /exemple.html :

Regardons maintenant la fentre de notre serveur :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. Serveur gnrique lanc sur le port 0.0.0.0:88 Client 127.0.0.1:4167 Tapez vos commandes (bye pour arrter) : <-- GET /exemple.html HTTP/1.1 <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.msxpsdocument, application/x-ms-xbap, application/x-ms-appl ication, application/x-silverlight, */* <-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3 <-- UA-CPU: x86 <-- Accept-Encoding: gzip, deflate <-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1. 4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022) <-- Host: localhost:88 <-- Connection: Keep-Alive <--

On dcouvre les enttes HTTP envoys par le navigateur. Cela nous permet de dcouvrir d'autres enttes HTTP que ceux dj rencontrs. Elaborons une rponse notre client. L'utilisateur au clavier est ici le vritable serveur et il peut laborer une rponse la main. Rappelons-nous la rponse faite par un serveur Web dans un prcdent exemple :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. HTTP/1.1 200 OK Date: Sat, 03 May 2008 07:53:47 GMT Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29 X-Powered-By: PHP/4.4.4-8+etch4 Set-Cookie: fe_typo_user=0d2e64b317; path=/ Connection: close Transfer-Encoding: chunked Content-Type: text/html;charset=iso-8859-1 693f <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR"> .... </html> 0

Programmation Internet

341

Essayons de donner une rponse analogue en s'en tenant au strict mimimum :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. HTTP/1.1 200 OK Server: serveur tcp generique Connection: close Content-Type: text/html

<html> <head><title>Serveur generique</title></head> <body><h2>Reponse du serveur generique</h2></body> </html> bye Flux de lecture des lignes de texte du client : l'erreur suivante s'est produite : Unable to read data from the transport connection: Une opration de blocage a t interrompue par un appel WSACancelBlockingCall. 12. [Fin du thread de lecture des demandes du client]

Nous nous sommes limits dans notre rponse aux enttes HTTP des lignes 1-4. Nous ne donnons pas la taille du document que nous allons envoyer (Content-Length) mais nous contentons de dire que nous allons fermer la connexion (Connection: close) aprs envoi de celui-ci. Cela est suffisant pour le navigateur. En voyant la connexion ferme, il saura que la rponse du serveur est termine et affichera la page HTML qui lui a t envoye. Cette dernire est celle des lignes 6-9. L'utilisateur au clavier ferme ensuite la connexion au client en tapant la commande bye, ligne 10. Sur cette commande clavier le thread principal ferme la connexion avec le client. Ceci provoque l'exception de la ligne 11. Le thread de lecture des lignes de texte du client a t interrompu brutalement par la fermeture de la liaison avec le client et a lanc une exception. Aprs la ligne 12, le serveur se met en attente d'un nouveau client. Le navigateur client affiche dsormais la chose suivante :

Si ci-dessus, on fait Affichage/Source pour voir ce qu'a reu le navigateur, on obtient [2], c'est dire exactement ce qu'on a envoy depuis le serveur gnrique. Le code du serveur TCP gnrique est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. using using using using using System; System.IO; System.Net; System.Net.Sockets; System.Threading;

namespace Chap9 { public class ServeurTcpGenerique { public const string syntaxe = "Syntaxe : ServeurGnrique Port"; // programme principal public static void Main(string[] args) { // y-a-t-il un argument ? if (args.Length != 1) { Console.WriteLine(syntaxe); Environment.Exit(1); } // cet argument doit tre entier >0 int port = 0; if (!int.TryParse(args[0], out port) || port <= 0) { Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine); Environment.Exit(2); } // on cre le service d'coute

Programmation Internet

342

26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96.

TcpListener ecoute = null; try { // on cre le service ecoute = new TcpListener(IPAddress.Any, port); // on le lance ecoute.Start(); // suivi Console.WriteLine("Serveur gnrique lanc sur le port {0}", ecoute.LocalEndpoint); while (true) { // attente d'un client Console.WriteLine("Attente du client suivant..."); TcpClient tcpClient = ecoute.AcceptTcpClient(); Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint); // on lance un thread part pour lire les lignes de texte envoyes par le client ThreadPool.QueueUserWorkItem(Receive, tcpClient); // la lecture des commandes clavier se fait dans le thread principal Console.WriteLine("Tapez vos commandes (bye pour arrter) : "); string rponse = null; // rponse serveur // on exploite la connexion client using (tcpClient) { // on cre un flux d'criture vers le client using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamWriter writer = new StreamWriter(networkStream)) { // flux de sortie non bufferis writer.AutoFlush = true; // boucle de saisie des rponses au clavier while (true) { rponse = Console.ReadLine(); // fini ? if (rponse.Trim().ToLower() == "bye") break; // on envoie la demande au client writer.WriteLine(rponse); } } } } } } catch (Exception ex) { // on signale l'erreur Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message); } finally { // fin de l'coute ecoute.Stop(); } } // thread de lecture serveur <-- client public static void Receive(object infos) { // donnes locales string demande = null; // demande du client string idClient=null; // identit du client

// exploitation connexion client try { using (TcpClient tcpClient = infos as TcpClient) { // identit client idClient = tcpClient.Client.RemoteEndPoint.ToString(); using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamReader reader = new StreamReader(networkStream)) { // boucle lecture en continu des lignes de texte du flux d'entre while ((demande = reader.ReadLine()) != null) { // affichage console Console.WriteLine("<-- {0}", demande); } } } } } catch (Exception ex) { // erreur Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient); 97. } finally { 98. // on signale la fin du thread de lecture 99. Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient); 100. }

Programmation Internet

343

101. 102. } 103.}

ligne 29 : le service d'coute est cr mais pas dmarr. Il coute toutes les interfaces rseau de la machine. ligne 31 : le service d'coute est dmarr ligne 34 : boucle infini d'attente des clients. L'utilisateur arrtera le serveur par Ctrl-C. ligne 37 : attente d'un client - opration bloquante. Lorsque le client arrive, l'instance TcpClient rendue par la mthode AcceptTcpClient reprsente le ct serveur d'une connexion ouverte avec le client. ligne 40 : le flux de lecture des demandes du client est confi un thread part. ligne 45 : utilisation de la connexion au client dans une clause using afin d'tre sr qu'elle sera ferme quoiqu'il arrive. ligne 47 : utilisation du flux rseau dans une clause using ligne 48 : cration dans une clause using d'un flux d'criture sur le flux rseau ligne 50 : le flux d'criture sera non bufferis lignes 52-59 : boucle de saisie au clavier des commandes envoyer au client ligne 69 : fin du service d'coute. Cette instruction ne sera jamais excute ici puisque le serveur est arrt par Ctrl-C. ligne 78 : la mthode Receive qui affiche en continu sur la console les lignes de texte envoyes par le client. On retrouve l ce qui a t vu pour le client TCP gnrique.

9.6.5

Un client Web

Nous avons vu dans l'exemple prcdent, certains des enttes HTTP qu'envoyait un navigateur :
1. 2. <-- GET /exemple.html HTTP/1.1 <-- Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/msword, application/xaml+xml, application/vnd.msxpsdocument, application/x-ms-xbap, application/x-ms-appl 3. ication, application/x-silverlight, */* 4. <-- Accept-Language: fr,en-US;q=0.7,fr-FR;q=0.3 5. <-- UA-CPU: x86 6. <-- Accept-Encoding: gzip, deflate 7. <-- User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1. 8. 4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.590; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022) 9. <-- Host: localhost:88 10. <-- Connection: Keep-Alive 11. <--

Nous allons crire un client Web auquel on passerait en paramtre une URL et qui afficherait l'cran le texte envoy par le serveur. Nous supposerons que celui-ci supporte le protocole HTTP 1.1. Des enttes prcdents, nous n'utiliserons que les suivants :
1. 2. 3. 4. <-- GET /exemple.html HTTP/1.1 <-- Host: localhost:88 <-- Connection: close <--

le premier entte indique le document dsir le second le serveur interrog le troisime que nous souhaitons que le serveur ferme la connexion aprs nous avoir rpondu.

Si ci-dessus ligne 1, nous remplaons GET par HEAD, le serveur ne nous enverra que les enttes HTTP et pas le document prcis ligne 1. Notre client web sera appel de la faon suivante : ClientWeb URL cmd, o URL est l'URL dsire et cmd l'un des deux mots cls GET ou HEAD pour indiquer si on souhaite seulement les enttes (HEAD) ou galement le contenu de la page (GET). Regardons un premier exemple :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. ...\Chap9\06\bin\Release>ClientWeb http://istia.univ-angers.fr:80 HEAD HTTP/1.1 200 OK Date: Sat, 03 May 2008 14:05:24 GMT Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29 X-Powered-By: PHP/4.4.4-8+etch4 Set-Cookie: fe_typo_user=e668408ac1; path=/ Connection: close Content-Type: text/html;charset=iso-8859-1 ...\Chap9\06\bin\Release>

Programmation Internet

344

ligne 1, nous ne demandons que les enttes HTTP (HEAD) lignes 2-9 : la rponse du serveur

Si nous utilisons GET au lieu de HEAD dans l'appel au client Web, nous obtenons le mme rsultat qu'avec HEAD avec de plus le corps du document demand. Le code du client web est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. using System; using System.IO; using System.Net.Sockets; namespace Chap9 { class ClientWeb { static void Main(string[] args) { // syntaxe const string syntaxe = "pg URI GET/HEAD"; // nombre d'arguments if (args.Length != 2) { Console.WriteLine(syntaxe); return; } // on note l'URI demande string stringURI = args[0]; string commande = args[1].ToUpper(); // vrification validit de l'URI if(! stringURI.StartsWith("http://")){ Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document"); return; } Uri uri = null; try { uri = new Uri(stringURI); } catch (Exception ex) { // URI incorrecte Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message); return; } // vrification de la commande if (commande != "GET" && commande != "HEAD") { // commande incorrecte Console.WriteLine("Le second paramtre doit tre GET ou HEAD"); return; } try { // on se connecte au service using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) { using (NetworkStream networkStream = tcpClient.GetStream()) { using (StreamReader reader = new StreamReader(networkStream)) { using (StreamWriter writer = new StreamWriter(networkStream)) { // flux de sortie non bufferis writer.AutoFlush = true; // on demande l'URL - envoi des enttes HTTP writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1"); writer.WriteLine("Host: " + uri.Host + ":" + uri.Port); writer.WriteLine("Connection: close"); writer.WriteLine(); // on lit la rponse string rponse = null; while ((rponse = reader.ReadLine()) != null) { // on affiche la rponse sur la console Console.WriteLine(rponse); } } } } } } catch (Exception e) { // on affiche l'exception Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message); } }

Programmation Internet

345

69. } 70. }

La seule nouveaut dans ce programme est l'utilisation de la classe Uri. Le programme reoit une URL (Uniform Resource Locator) ou URI (Uniform Resource Identifier) de la forme http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... La classe Uri nous permet de dcomposer la chane de l'URL en ses diffrents lments.

lignes 26-33 : un objet Uri est construit partir de la chane stringURI reue en paramtre. Si la chane URI reue en paramtre n'est pas une URI valide (absence du protocole, du serveur, ...), une exception est lance. Cela nous permet de vrifier la validit du paramtre reu. Une fois l'objet Uri construit, on a accs aux diffrents lments de cette Uri. Ainsi si l'objet uri du code prcdent a t construit partir de la chane http://serveur:port/document?param1=val1&param2=val2;... on aura : uri.Host=serveur, uri.Port=port, uri.Path=document, uri.Query=param1=val1&param2=val2;..., uri.pathAndQuery= cheminPageHTML?param1=val1&param2=val2;..., uri.Scheme=http.

9.6.6

Un client Web grant les redirections

Le client Web prcdent ne gre pas une ventuelle redirection de l'URL qu'il a demande. Voici un exemple :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. ...\Chap9\06\bin\Release>ClientWeb http://www.ibm.com GET HTTP/1.1 302 Found Date: Sat, 03 May 2008 14:50:52 GMT Server: IBM_HTTP_Server Location: http://www.ibm.com/us/ Content-Length: 206 Kp-eeAlive: timeout=10, max=73 Connection: Keep-Alive Content-Type: text/html <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>302 Found</title> </head><body> <h1>Found</h1> <p>The document has moved <a href="http://www.ibm.com/us/">here</a>.</p> </body></html>

ligne 2 : le code 302 Found indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est dans le corps du document, ligne 16.

Un deuxime exemple :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. ...\Chap9\06\bin\Release>ClientWeb http://www.bull.com GET HTTP/1.1 301 Moved Permanently Date: Sat, 03 May 2008 14:52:31 GMT Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4 X-Powered-By: PHP/4.3.4 Location: http://www.bull.com/index.php Connection: close Transfer-Encoding: chunked Content-Type: text/html 0

ligne 2 : le code 301 Moved Permanently indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est indique ligne 6, dans l'entte HTTP Location.

Un troisime exemple :
1. 2. 3. 4. 5. 6. 7. ...\Chap9\06\bin\Release>ClientWeb http://www.gouv.fr GET HTTP/1.1 302 Moved Temporarily Server: AkamaiGHost Content-Length: 0 Location: http://www.premier-ministre.gouv.fr/fr/ Date: Sat, 03 May 2008 14:56:53 GMT Connection: close

Programmation Internet

346

ligne 2 : le code 302 Moved Temporarily indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est indique ligne 5, dans l'entte HTTP Location.

Un quatrime exemple avec un serveur IIS local la machine :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. ...\istia\Chap9\06\bin\Release>ClientWeb.exe http://localhost HEAD HTTP/1.1 302 Object moved Server: Microsoft-IIS/5.1 Date: Sun, 04 May 2008 10:16:56 GMT Connection: close Location: localstart.asp Content-Length: 121 Content-Type: text/html Set-Cookie: ASPSESSIONIDQQASDQAB=FDJLADLCOLDHGKGNIPMLHIIA; path=/ Cache-control: private

ligne 2 : le code 302 Object moved indique une redirection. L'adresse vers laquelle le navigateur doit se rediriger est indique ligne 5, dans l'entte HTTP Location. On notera que contrairement aux exemples prcdents, l'adresse de redirection est relative. L'adresse complte est en fait http://localhost/localstart.asp.

Nous nous proposons de grer les redirections lorsque la premire ligne des enttes HTTP contient le mot cl moved (insensible la casse) et que l'adresse de redirection est dans l'entte HTTP Location. Si nous reprenons les trois derniers exemples, nous avons les rsultats suivants : Url : http://www.bull.com
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. ...\Chap9\06B\bin\Release>ClientWebAvecRedirection http://www.bull.com HEAD HTTP/1.1 301 Moved Permanently Date: Sun, 04 May 2008 10:22:48 GMT Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4 X-Powered-By: PHP/4.3.4 Location: http://www.bull.com/index.php Connection: close Content-Type: text/html <--Redirection vers l'URL http://www.bull.com/index.php--> HTTP/1.1 200 OK Date: Sun, 04 May 2008 10:22:49 GMT Server: Apache/1.3.33 (Unix) WS_filter/2.1.15 PHP/4.3.4 X-Powered-By: PHP/4.3.4 Connection: close Content-Type: text/html

ligne 11 : la redirection a lieu vers l'adresse de la ligne 6

Url : http://www.gouv.fr
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. ...\Chap9\06B\bin\Release>ClientWebAvecRedirect ion http://www.gouv.fr HEAD HTTP/1.1 302 Moved Temporarily Server: AkamaiGHost Content-Length: 0 Location: http://www.premier-ministre.gouv.fr/fr/ Date: Sun, 04 May 2008 10:30:38 GMT Connection: close <--Redirection vers l'URL http://www.premier-ministre.gouv.fr/fr/--> HTTP/1.1 200 OK Server: Apache X-Powered-By: PHP/4.4.1 Last-Modified: Sun, 04 May 2008 10:29:48 GMT Content-Type: text/html Expires: Sun, 04 May 2008 10:40:38 GMT Date: Sun, 04 May 2008 10:30:38 GMT Connection: close

Programmation Internet

347

ligne 11 : la redirection a lieu vers l'adresse de la ligne 6

Url : http://localhost
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. ...\Chap9\06B\bin\Release>ClientWebAvecRedirection.exe http://localhost HEAD HTTP/1.1 302 Object moved Server: Microsoft-IIS/5.1 Date: Sun, 04 May 2008 10:37:11 GMT Connection: close Location: localstart.asp Content-Length: 121 Content-Type: text/html Set-Cookie: ASPSESSIONIDQQASDQAB=GDJLADLCJCMPCHFFEJEFPKMK; path=/ Cache-control: private <--Redirection vers l'URL http://localhost/localstart.asp--> HTTP/1.1 401 Access Denied Server: Microsoft-IIS/5.1 Date: Sun, 04 May 2008 10:37:11 GMT WWW-Authenticate: Negotiate WWW-Authenticate: NTLM WWW-Authenticate: Basic realm="localhost" Connection: close Content-Length: 4766 Content-Type: text/html

ligne 13 : la redirection a lieu vers l'adresse de la ligne 6 ligne 15 : l'accs la page http://localhost/localstart.asp nous a t refus.

Le programme grant la redirection est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. using using using using System; System.IO; System.Net.Sockets; System.Text.RegularExpressions;

namespace Chap9 { class ClientWebAvecRedirection { static void Main(string[] args) { // syntaxe const string syntaxe = "pg URI GET/HEAD"; // nombre d'arguments if (args.Length != 2) { Console.WriteLine(syntaxe); return; } // on note l'URI demande string stringURI = args[0]; string commande = args[1].ToUpper(); // vrification validit de l'URI if (!stringURI.StartsWith("http://")) { Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document"); return; } Uri uri = null; try { uri = new Uri(stringURI); } catch (Exception ex) { // URI incorrecte Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message); return; } // vrification de la commande if (commande != "GET" && commande != "HEAD") { // commande incorrecte Console.WriteLine("Le second paramtre doit tre GET ou HEAD"); return; } const int nbRedirsMax = 1; int nbRedirs = 0; // pas plus d'une redirection accepte // nombre de redirections en cours

Programmation Internet

348

44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77.

// expression rgulire pour trouver une URL de redirection Regex location = new Regex(@"^Location: (.+?)$"); try { // on peut avoir plusieurs URL demander s'il y a des redirections while (nbRedirs <= nbRedirsMax) { // gestion redirection bool redir = false; bool locationFound = false; string locationString = null; // on se connecte au service using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) { using (StreamReader reader = new StreamReader(tcpClient.GetStream())) { using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) { // flux de sortie non bufferis writer.AutoFlush = true; // on demande l'URL - envoi des enttes HTTP writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1"); writer.WriteLine("Host: " + uri.Host + ":" + uri.Port); writer.WriteLine("Connection: close"); writer.WriteLine(); // on lit la premire ligne de la rponse string premireLigne = reader.ReadLine(); // cho cran Console.WriteLine(premireLigne); // redirection ? if (Regex.IsMatch(premireLigne.ToLower(), @"\s+moved\s*")) { // il y a une redirection redir = true; nbRedirs++; } enttes // enttes HTTP suivants jusqu' trouver la ligne vide signalant la fin des string rponse = null; while ((rponse = reader.ReadLine()) != "") { // on affiche la rponse Console.WriteLine(rponse); // s'il y a redirection, on recherche l'entte Location if (redir && !locationFound) { // on compare la ligne courante l'expression relationnelle location Match rsultat = location.Match(rponse); if (rsultat.Success) { // si on a trouv, on note l'URL de redirection locationString = rsultat.Groups[1].Value; // on note qu'on a trouv locationFound = true; } } } // les enttes HTTP ont t puiss - on crit la ligne vide Console.WriteLine(rponse); // puis on passe au corps du document while ((rponse = reader.ReadLine()) != null) { Console.WriteLine(rponse); }

78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112. 113. 114. 115. 116. 117. 118. 119.

} }

} // a-t-on fini ? if (!locationFound || nbRedirs > nbRedirsMax) break; // il y a une redirection oprer - on construit la nouvelle Uri try { if (locationString.StartsWith("http")) { // adresse http complte uri = new Uri(locationString); } else { // adresse http relative l'uri courante uri = new Uri(uri, locationString); } // log console Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri); } catch (Exception ex) { // pb avec l'Uri

Programmation Internet

349

120. Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas t comprise : {1} ->\n", locationString, ex.Message); 121. } 122. } 123. } catch (Exception e) { 124. // on affiche l'exception 125. Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message); 126. } 127. } 128. } 129.}

Par rapport la version prcdente, les changements sont les suivants :


ligne 46 : l'expression rgulire pour rcuprer l'adresse de redirection dans l'entte HTTP Location: adresse. ligne 49 : le code qui tait excut prcdemment pour une unique Uri peut l'tre maintenant successivement pour plusieurs Uri. ligne 66 : on lit la 1re ligne des enttes HTTP envoys par le serveur. C'est elle qui contient le mot cl moved si le document demand a t dplac. lignes 71-75 : on vrifie si la 1re ligne contient le mot cl moved. Si oui, on le note. lignes 79-93 : lecture des autres enttes HTTP jusqu' rencontrer la ligne vide qui signale leur fin. Si la 1re ligne annonait une redirection, on s'attarde alors sur l'entte HTTP Location: adresse pour mmoriser l'adresse de redirection dans locationString. lignes 98-100 : le reste de la rponse du serveur HTTP est affich la console. lignes 105-106 : l'Uri demande a t entirement exploite et affiche. S'il n'y a pas de redirection faire ou si le nombre de redirections autorises est dpass, on quitte le programme. lignes 108-122 : s'il y a redirection, on calcule la nouvelle Uri demander. Il y a une petite gymnastique faire selon que l'adresse de redirection trouve tait absolue (ligne 111) ou relative (ligne 114).

9.7

Les classes .NET spcialises dans un protocole particulier de l'internet

Dans les exemples prcdents du client web, le protocole HTTP tait gr avec un client TCP. Il nous fallait donc grer nousmmes le protocole de communication particulier utilis. Nous aurions pu construire de faon analogue, un client SMTP ou POP. Le framework .NET offre des classes spcialises pour les protocoles HTTP et SMTP. Ces classes connaissent le protocole de communication entre le client et le serveur et vitent au dveloppeur d'avoir les grer. Nous les prsentons maintenant.

9.7.1

La classe WebClient

Il existe une classe WebClient sachant dialoguer avec un serveur web. Considrons l'exemple du client web du paragraphe 9.6.5, page 344, trait ici avec la classe WebClient.
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. using System; using System.IO; using System.Net; namespace Chap9 { public class Program { public static void Main(string[] args) { // syntaxe : [prog] Uri const string syntaxe = "pg URI"; // nombre d'arguments if (args.Length != 1) { Console.WriteLine(syntaxe); return; } // on note l'URI demande string stringURI = args[0]; // vrification validit de l'URI if (!stringURI.StartsWith("http://")) { Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document"); return; } Uri uri = null; try { uri = new Uri(stringURI); } catch (Exception ex) {

Programmation Internet

350

28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. } 58. }

// URI incorrecte Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message); return; } try { // cration client web using (WebClient client = new WebClient()) { // ajout d'un entte HTTP client.Headers.Add("user-agent", "st"); using (Stream stream = client.OpenRead(uri)) { using (StreamReader reader = new StreamReader(stream)) { // affichage rponse du serveur web Console.WriteLine(reader.ReadToEnd()); // affichage enttes rponse du serveur Console.WriteLine("---------------------"); foreach (string cl in client.ResponseHeaders.Keys) { Console.WriteLine("{0}: {1}", cl, client.ResponseHeaders[cl]); } Console.WriteLine("---------------------"); } } } } catch (WebException e1) { Console.WriteLine("L'exception suivante s'est produite : {0}", e1); } catch (Exception e2) { Console.WriteLine("L'exception suivante s'est produite : {0}", e2); }

ligne 35 : le client web est cr mais pas encore configur ligne 37 : on ajoute un entte HTTP la demande HTTP qui va tre faite. Nous allons dcouvrir que d'autres enttes seront envoys par dfaut. ligne 38 : le client web demande l'Uri donne par l'utilisateur et lit le document envoy. [WebClient].OpenRead(Uri) ouvre la connexion avec Uri et lit la rponse. C'est l l'intrt de la classe. Elle s'occupe du dialogue avec le serveur web. Le rsultat de la mthode OpenRead est de type Stream et reprsente le document demand. Les enttes HTTP envoys par le serveur et qui prcdent le document dans la rponse n'en font pas partie. ligne 39 : on utilise un StreamReader et ligne 41, sa mthode ReadToEnd pour lire la totalit de la rponse. lignes 44-46 : on affiche les enttes HTTP de la rponse du serveur. [WebClient].ResponseHeaders reprsente une collection value dont les cls sont les noms des enttes HTTP et les valeurs, les chanes de caractres associes ces enttes. ligne 51 : les exceptions qui sont leves lors d'un change client / serveur sont de type WebException.

Voyons quelques exemples. On lance le serveur TCP gnrique construit au paragraphe 4.4.6, page 156 :
1. 2. ...\Chap9\05\bin\Release>ServeurTcpGenerique.exe 88 Serveur gnrique lanc sur le port 0.0.0.0:88

On lance le client web prcdent de la faon suivante :


...\Chap9\09\bin\Release>09 http://localhost:88

L'Uri demande est celle du serveur gnrique. Celui-ci affiche alors les enttes HTTP que lui a envoys le client web :
1. 2. 3. 4. 5. 6. 7. Client 127.0.0.1:1415 Tapez vos commandes (bye pour arrter) : <-- GET / HTTP/1.1 <-- User-Agent: st <-- Host: localhost:88 <-- Connection: Keep-Alive <--

On voit ainsi : que le client web envoie 3 enttes HTTP par dfaut (lignes 3, 5, 6) ligne 4 : l'entte que nous avons gnr nous-mmes (ligne 37 du code) que le client web utilise par dfaut la mthode GET (ligne 3). Il existe d'autres mthodes parmi lesquelles POST et HEAD.

Programmation Internet

351

Maintenant demandons une ressource inexistante :


1. 2. 3. 4. 5. ...\Chap9\09\bin\Release>09 http://istia.univ-angers.fr/inconnu L'exception suivante s'est produite : System.Net.WebException: The remote server returned an error: (404) Not Found. at System.Net.WebClient.OpenRead(Uri address) at System.Net.WebClient.OpenRead(String address) at Chap9.WebClient1.Main(String[] args) in C:\data\2007-2008\c# 2008\poly\istia\Chap9\09\Program.cs:line 16

ligne 2 : on a eu une exception de type WebException parce que le serveur a rpondu par le code 404 Not Found pour indiquer que la ressource demande n'existait pas.

Enfin terminons en demandant une ressource existante :


...\istia\Chap9\09\bin\Release>09 http://istia.univ-angers.fr >istia.univ-angers.txt

Le fichier istia.univ-angers.txt produit par la commande est le suivant :


1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr_FR" lang="fr_FR"> <head> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> ... </html> 2. --------------------3. Keep-Alive: timeout=15, max=100 4. Connection: Keep-Alive 5. Transfer-Encoding: chunked 6. Content-Type: text/html;charset=iso-8859-1 7. Date: Sun, 04 May 2008 14:30:53 GMT 8. Set-Cookie: fe_typo_user=22eaaf283a; path=/ 9. Server: Apache/1.3.34 (Debian) PHP/4.4.4-8+etch4 mod_jk/1.2.18 mod_perl/1.29 10. X-Powered-By: PHP/4.4.4-8+etch4 11. ---------------------

ligne 1 : le document HTML demand. lignes 3-10 : les enttes de la rponse HTTP dans un ordre qui n'est pas forcment celui dans lequel ils ont t envoys.

La classe WebClient dispose de mthodes permettant de recevoir un document (mthodes DownLoad) ou d'en envoyer (mthodes UpLoad) :
DownLoadData DownLoadFile DownLoadString OpenWrite UpLoadData UpLoadFile UpLoadString UpLoadValues

pour tlcharger une ressource en tant que tableau d'octets (image par exemple) pour tlcharger une ressource et la sauvegarder dans un fichier local pour tlcharger une ressource et la rcuprer en tant que chane de caractres (fichier html par exemple) le pendant d'OpenRead mais pour envoyer des donnes au serveur le pendant de DownLoadData mais vers le serveur le pendant de DownLoadFile mais vers le serveur le pendant de DownLoadString mais vers le serveur pour envoyer au serveur les donnes d'une commande POST et en rcuprer les rsultats sous la forme d'un tableau d'octets. La commande POST demande un document tout en transmettant au serveur des informations qui lui sont ncessaires pour dterminer le document rel envoyer. Ces informations sont envoyes comme document au serveur, d'o le nom UpLoad de la mthode. Elles sont envoyes derrire la ligne vide des enttes HTTP sous la forme param1=valeur1&param2=valeur2&... :
POST /document HTTP/1.1 ... [ligne vide] param1=valeur1&param2=valeur2&...

Le mme document pourrait tre demand avec la mthode GET :


GET /document?param1=valeur1&param2=valeur2&...

Programmation Internet

352

... [ligne vide]

La diffrence entre les deux mthodes est que le navigateur affichant l'Uri demande, affichera /document dans le cas du POST et /document?param1=valeur1&param2=valeur2&... dans le cas du GET.

9.7.2

Les classes WebRequest / WebResponse

Parfois la classe WebClient n'est pas suffisamment souple pour faire ce que l'on souhaite. Reprenons l'exemple du client web avec redirection tudi au paragraphe 9.6.6, page 346. Il nous faut mettre l'entte HTTP :
HEAD /document HTTP/1.1

Nous avons vu que les enttes HTTP mis par dfaut par le client web taient les suivants :
1. 2. 3. <-- GET / HTTP/1.1 <-- Host: machine:port <-- Connection: Keep-Alive

Nous avons vu galement qu'il tait possible d'ajouter des enttes HTTP aux prcdents avec la collection [WebClient].Headers. Seulement la ligne 1 n'est pas un entte appartenant la collection Headers car elle n'a pas la forme cl: valeur. Je n'ai pas trouv comment changer le GET en HEAD dans la ligne 1 en partant de la classe WebClient (j'ai peut-tre mal cherch ?). Lorsque la classe WebClient a atteint ses limites, on peut passer aux classes WebRequest / WebResponse : WebRequest : reprsente la totalit de la demande du client Web. WebResponse : reprsente la totalit de la rponse du serveur Web Nous avons dit que la classe WebClient grait les schmas http:, https:, ftp:, file:. Les requtes et rponses de ces diffrents protocoles n'ont pas la mme forme. Aussi est-il ncessaire de manipuler le type exact de ces lments plutt que leur type gnrique WebRequest et WebResponse. Aussi utilisera-t-on les classes :

HttpWebRequest, HttpWebResponse pour un client HTTP FtpWebRequest, FtpWebResponse pour un client FTP

Nous traitons maintenant avec les classes HttpWebRequest et HttpWebresponse l'exemple du client web avec redirection tudi au paragraphe 9.6.6, page 346. Le code est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. using using using using System; System.IO; System.Net.Sockets; System.Net;

namespace Chap9 { class WebRequestResponse { static void Main(string[] args) { // syntaxe const string syntaxe = "pg URI GET/HEAD"; // nombre d'arguments if (args.Length != 2) { Console.WriteLine(syntaxe); return; } // on note l'URI demande string stringURI = args[0]; string commande = args[1].ToUpper(); // vrification validit de l'URI Uri uri = null; try { uri = new Uri(stringURI); } catch (Exception ex) { // URI incorrecte Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message); return; } // vrification de la commande if (commande != "GET" && commande != "HEAD") {

Programmation Internet

353

33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71.

// commande incorrecte Console.WriteLine("Le second paramtre doit tre GET ou HEAD"); return; } try { // on configure la requte HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest; httpWebRequest.Method = commande; httpWebRequest.Proxy = null; // on l'excute HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse; // rsultat Console.WriteLine("---------------------"); Console.WriteLine("Le serveur {0} a rpondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription); // enttes HTTP Console.WriteLine("---------------------"); foreach (string cl in httpWebResponse.Headers.Keys) { Console.WriteLine("{0}: {1}", cl, httpWebResponse.Headers[cl]); } Console.WriteLine("---------------------"); // document using (Stream stream = httpWebResponse.GetResponseStream()) { using (StreamReader reader = new StreamReader(stream)) { // on affiche la rponse sur la console Console.WriteLine(reader.ReadToEnd()); } } } catch (WebException e1) { // on rcupre la rponse HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse; Console.WriteLine("Le serveur {0} a rpondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription); } catch (Exception e2) { // on affiche l'exception Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message); } } } }

ligne 40 : un objet de type WebRequest est cr avec la mthode statique WebRequest.Create(Uri uri) ou uri est l'uri du document tlcharger. Parce que l'on sait que le protocole de l'Uri est HTTP, le type du rsultat est chang en HttpWebRequest afin d'avoir accs aux lments spcifiques du protocole Http. ligne 41 : nous fixons la mthode GET / POST / HEAD de la 1re ligne des enttes HTTP. Ici ce sera GET ou HEAD. ligne 42 : dans un rseau priv d'entreprise, il est frquent que les machines de l'entreprise soit isoles de l'internet pour des raisons de scurit. Pour cela, le rseau priv utilise des adresses internet que les routeurs de l'internet ne routent pas. Le rseau priv est reli l'internet par des machines particulires appeles proxy qui sont relies la fois au rseau priv de l'entreprise et l'internet. C'est un exemple de machines plusieurs adresses IP. Une machine du rseau priv ne peut tablir elle-mme une connexion avec un serveur de l'internet, un serveur web par exemple. Elle doit demander une machine proxy de le faire pour elle. Une machine proxy peut abriter des serveurs proxy pour diffrents protocoles. On parle de proxy HTTP pour dsigner le service qui s'occupe de faire les requtes HTTP pour le compte des machines du rseau priv. Si un tel serveur proxy HTTP existe, il faut l'indiquer dans le champ [WebRequest].proxy. On crira par exemple :
[WebRequest].proxy=new WebProxy("pproxy.istia.uang:3128");

si le proxy HTTP opre sur le port 3128 de la machine pproxy.istia.uang. On met null dans le champ [WebRequest].proxy si la machine a un accs direct l'internet et n'a pas passer par un proxy. ligne 44 : la mthode GetResponse() demande le document identifi par son Uri et rend un objet WebRequestResponse qu'on transforme ici en objet HttpWebResponse. Cet objet reprsente la rponse du serveur la demande du document. ligne 47 : [HttpWebResponse].ResponseUri : est l'Uri du serveur ayant envoy le document. En cas de redirection, celle-ci peut tre diffrente de l'Uri du serveur interrog initialement. On notera que le code ne gre pas la redirection. Elle est gre automatiquement par la mthode GetResponse. De nouveau, c'est l'avantage des classes de haut niveau vis vis des classes basiques du protocole Tcp. [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription reprsentent la 1re ligne de la rponse, par exemple : HTTP/1.1 200 OK. StatusCode est 200 et StatusDescription est OK. ligne 50 : [HttpWebResponse].Headers est la collection des enttes HTTP de la rponse. ligne 55 : [HttpWebResponse].GetResponseStream : est le flux qui permet d'obtenir le document contenu dans la rponse. ligne 61 : il peut se produire une exception de type WebException

Programmation Internet

354

ligne 63 : [WebException].Response est la rponse qui a provoqu la leve de l'exception.

Voici un exemple d'excution :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. ...\Chap9\09B\bin\Release>09B http://www.gouv.fr HEAD --------------------Le serveur http://www.premier-ministre.gouv.fr/fr/ a rpondu : 200 OK --------------------Connection: keep-alive Content-Type: text/html; charset=iso-8859-1 Date: Mon, 05 May 2008 13:02:29 GMT Expires: Mon, 05 May 2008 13:07:20 GMT Last-Modified: Mon, 05 May 2008 12:56:59 GMT Server: Apache X-Powered-By: PHP/4.4.1 ---------------------

lignes 1 et 3 : le serveur qui a rpondu n'est pas le mme que celui qui avait t interrog. Il y a donc eu redirection. lignes 5-11 : les enttes HTTP envoys par le serveur

9.8

Application : un client proxy pour un serveur web de traduction

Nous montrons maintenant comment les classes prcdentes nous permettent d'exploiter les ressources du web.

9.8.1

L'application

Il existe sur le web des sites de traduction. Celui qui sera utilis ici est le site http://trans.voila.fr/traduction_voila.php : Le texte traduire est insr dans [1], le sens de traduction est choisi dans [2]. La traduction est demande par [3] et obtenue en [4]. 2

1 3

Nous allons crire une application windows cliente de l'application ci-dessus. Elle ne fera rien de plus que l'application du site [trans.voila.fr]. Son interface sera la suivante :

2 3

Programmation Internet

355

9.8.2

L'architecture de l'application

L'application aura l'architecture 2 couches suivante :

utilisateur

Couche ui [ui]

Couche d'accs aux donnes [dao] DLL Internet

Serveur de traduction

Couche [entites] SPRING

9.8.3

Le projet Visual studio

Le projet Visual studio sera le suivant : 3 1

en [1], la solution est compose de deux projets, [2] : l'un pour la couche [dao] et les entits utilises par celle-ci, [3] : l'autre pour l'interface windows

9.8.4

Le projet [dao]

Le projet [dao] est form des lments suivants : IServiceTraduction.cs : l'interface prsente la couche [ui] ServiceTraduction : l'implmentation de cette interface WebTraductionsException : une exception spcifique l'application L'interface IServiceTraduction est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using System.Collections.Generic; namespace dao { public interface IServiceTraduction { // langues utilises IDictionary<string, string> LanguesTraduites { get; } // traduction string Traduire(string texte, string deQuoiVersQuoi); } }

ligne 6 : la proprit LanguesTraduites rend le dictionnaire des langues acceptes par le serveur de traduction. Ce dictionnaire a des entres de la forme ["fe","Franais-Anglais"] o la valeur dsigne un sens de traduction, ici du Franais vers l'Anglais, et la cl "fe" est un code utilis par le serveur de traduction trans.voila.fr. ligne 8 : la mthode Traduire est la mthode de traduction : texte est le texte traduire deQuoiVersQuoi est l'une des cls du dictionnaire des langues traduites la mthode rend la traduction du texte

Programmation Internet

356

ServiceTraduction est une classe d'implmentation de l'interface IServiceTraduction. Nous la dtaillons dans la section qui suit. WebTraductionsException est la classe d'exception suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. using System; namespace entites { public class WebTraductionsException : Exception { // code d'erreur public int Code { get; set; } // constructeurs public WebTraductionsException() { } public WebTraductionsException(string message) : base(message) { } public WebTraductionsException(string message, Exception e) : base(message, e) { } } }

ligne 7 : un code d'erreur

9.8.5

Le client web [ServiceTraduction]

Revenons sur l'architecture de notre application :

utilisateur

Couche ui [ui]

[ServiceTraduction]

Couche [entites] SPRING

Internet

Serveur de traduction [trans.voila.fr]

La classe [ServiceTraduction] que nous devons crire est un client du service web de traduction [trans.voila.fr]. Pour l'crire, il nous faut comprendre ce qu'attend le serveur de traduction de son client ce qu'il renvoie en retour son client Voyons sur un exemple le dialogue client / serveur qui intervient dans une traduction. Reprenons l'exemple prsent en introduction de l'application : Le texte traduire est insr dans [1], le sens de traduction est choisi dans [2]. La traduction est demande par [3] et obtenue en [4]. 2

1 3

Pour obtenir la traduction [4], le navigateur a envoy la requte GET suivante (affiche dans son champ d'adresse) :

Programmation Internet

357

http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection=fe&stext=ce+chien+est+malade

Elle est plutt simple comprendre : http://trans.voila.fr/traduction_voila.php est l'Url du service de traduction isText=1 semble vouloir dire qu'on a affaire du texte translationDirection dsigne le sens de la traduction, ici Franais-Anglais stext est le texte traduire sous une forme qu'on appelle Url encode. En effet, certains caractres ne peuvent apparatre dans une Url. C'est le cas par exemple de l'espace qui a t ici encod par un +. Le framework .Net offre la mthode statique System.Web.HttpUtility.UrlEncode pour faire ce travail d'encodage. On en conclut que pour interroger le serveur de traduction, notre classe [ServiceTraduction] pourra utiliser la chane
"http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"

o les marqueurs {0} et {1} seront remplacs respectivement par le sens de traduction et le texte traduire. Comment connait-on les sens de traduction accepts par le serveur ? Dans la copie d'cran ci-dessus, les langues traduites sont dans la liste droulante. Si dans le navigateur on regarde (Affichage / source) le code Html de la page, on trouce ceci pour la liste droulante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. <select name="translationDirection" class="champs"> <option selected value='fe'>Fran&ccedil;ais vers Anglais <option value='ef'>Anglais vers Fran&ccedil;ais <option value='fg'>Fran&ccedil;ais vers Allemand <option value='gf'>Allemand vers Fran&ccedil;ais <option value='fs'>Fran&ccedil;ais vers Espagnol <option value='sf'>Espagnol vers Fran&ccedil;ais <option value='fr'>Fran&ccedil;ais vers Russe <option value='rf'>Russe vers Fran&ccedil;ais <option value='es'>Anglais vers Espagnol <option value='se'>Espagnol vers Anglais <option value='eg'>Anglais vers Allemand <option value='ge'>Allemand vers Anglais <option value='ep'>Anglais vers Portugais <option value='pe'>Portugais vers Anglais <option value='ie'>Italien vers Anglais <option value='gs'>Allemand vers Espagnol <option value='sg'>Espagnol vers Allemand </select>

Ce n'est pas un code Html trs propre, dans la mesure o chaque balise <option> devrait tre normalement ferme par une balise </option>. Ceci dit, les attributs value nous donnent la liste des codes de traduction qui doivent tre envoys au serveur. Dans le dictionnaire LanguesTraduites de l'interface IServiceTraduction, les cls seront les attributs value ci-dessus et les valeurs, les textes affichs par la liste droulante. Maintenant regardons (Affichage / source) o se trouve dans la page Html la traduction renvoye par le serveur de traduction :
... <strong>Texte traduit : </strong><div class="txtTrad">this dog is sick</div> ...

La traduction se trouve au beau milieu de la page Html renvoye. Comment la retrouver ? On peut utiliser une expression rgulire avec la squence <div class="txtTrad">...</div> car la balise <div class="txtTrad"> n'est prsente qu' cet endroit de la page Html. L'expression rgulire C# permettant de rcuprer le texte traduit est la suivante :
@"<div class=""txtTrad"">(.*?)</div>"

Nous avons dsormais les lments pour crire la classe d'implmentation ServiceTraduction de l'interface IServiceTraduction :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. using using using using using using using System; System.Collections.Generic; System.IO; System.Net; System.Text.RegularExpressions; System.Web; entites;

namespace dao { public class ServiceTraduction : IServiceTraduction {

Programmation Internet

358

11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67. 68. 69. 70. 71.

// proprits automatiques de configuration du service public IDictionary<string, string> LanguesTraduites { get; set; } public string UrlServeurTraduction { get; set; } public string ProxyHttp { get; set; } public String RegexTraduction { get; set; } // traduction public string Traduire(string texte, string deQuoiVersQuoi) { // la traduction demande est-elle possible ? if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) { throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 }; } // texte traduire string texteATraduire = HttpUtility.UrlEncode(texte); // uri demander string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire); // expression rgulire pour retrouver la traduction dans la rponse Regex patternTraduction = new Regex(RegexTraduction); // exception WebTraductionsException exception = null; // traduction string traduction = null; try { // on configure la requte HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest; httpWebRequest.Method = "GET"; httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ; // on l'excute HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse; // document using (Stream stream = httpWebResponse.GetResponseStream()) { using (StreamReader reader = new StreamReader(stream)) { bool traductionTrouve = false; string ligne = null; while (!traductionTrouve && (ligne = reader.ReadLine()) != null) { // recherche traduction dans la ligne courante MatchCollection rsultats = patternTraduction.Matches(ligne); // traduction trouve ? if (rsultats.Count != 0) { traduction = rsultats[0].Groups[1].Value.Trim(); traductionTrouve = true; } } // traduction trouve ? if (!traductionTrouve) { exception = new WebTraductionsException("Le serveur n'a pas renvoy de rponse") { Code = 12 }; } } } } catch (Exception e) { exception = new WebTraductionsException("Erreur rencontre lors de la traduction", e) { Code = 11 }; } // exception ? if (exception != null) { throw exception; } else { return traduction; } } } }

ligne 12 : la proprit LanguesTraduites de l'interface IServiceTraduction - initialise de l'extrieur ligne 13 : la proprit UrlServeurTraduction est l'Url demander au serveur de traduction : http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1} o le marqueur {0} devra tre remplac par le sens de traduction et le marqueur {1} par le texte traduire - initialise de l'extrieur ligne 14 : la proprit ProxyHttp est l'ventuel proxy Http utiliser, par exemple : pproxy.istia.uang:3128 - initialise de l'extrieur ligne 15 : la proprit RegexTraduction est l'expression rgulire permettant de rcuprer la traduction dans le flux Html renvoy par le serveur de traduction, par exemple @"<div class=""txtTrad"">(.*?)</div>" - initialise de l'extrieur ces quatres proprits seront, dans notre application, initialises par Spring.

Programmation Internet

359

lignes 20-22 : on vrifie que le sens de traduction demand existe bien dans le dictionnaire des langues traduites. Si ce n'est pas le cas, une exception est lance. ligne 24 : le texte traduire est encod pour pouvoir faire partie d'une Url ligne 26 : l'Uri du service de traduction est construite. Si la proprit UrlServeurTraduction est la chane http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}, le marqueur {0} est remplac par le sens de traduction et le marqueur {1} par le texte traduire. ligne 28 : le modle de recherche de la traduction dans la rponse html du serveur de traduction est construit. lignes 33, 60 : l'opration d'interrogation du serveur de traduction se passe dans un try / catch ligne 35 : l'objet HttpWebRequest qui va tre utilis pour interroger le serveur de traduction est construit avec l'Uri du document demand. ligne 36 : la mthode d'interrogation est GET. On pourrait se passer de cette instruction, car GET est probablement la mthode par dfaut de l'objet HttpWebRequest. ligne 37 : on fixe la proprit Proxy de l'objet HttpWebRequest. ligne 39 : la requte au serveur de traduction est faite et on rcupre sa rponse qui est de type HttpWebResponse. lignes 41-42 : on utilise un StreamReader pour lire chaque ligne de la rponse html du serveur. lignes 45-53 : dans chaque ligne de la rponse, on cherche la traduction. Lorsqu'on l'a trouve, on arrte de lire la rponse Html et on ferme tous les flux qu'on a ouverts. lignes 55-57 : si on n'a pas trouv de traduction dans la rponse html, on prpare une exception de type WebTraductionsException pour le dire. lignes 60-62 : si une exception s'est produite lors de l'cange client / serveur, on l'encapsule dans une exception de type WebTraductionsException pour le dire. lignes 64-68 : si une exception a t enregistre, elle est lance, sinon la traduction trouve est rendue.

Notre exemple suppose que le proxy Http ne ncessite pas d'authentification. Si ce n'tait pas le cas, on crirait quelque chose comme :
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ; httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");

Nous avons utilis ici WebRequest / WebResponse plutt que WebClient parce que nous n'avons pas exploiter la totalit de la rponse Html du serveur de traduction. Une fois la traduction trouve dans cette rponse, nous n'avons plus besoin du reste des lignes de la rponse. La classe WebClient ne permet pas de faire cela. Voici un programme de test de la classe ServiceTraduction :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. using using using using System; System.Collections.Generic; dao; entites;

namespace ui { class Program { static void Main(string[] args) { try { // cration service traduction ServiceTraduction serviceTraduction = new ServiceTraduction(); // expression rgulire pour trouver la traduction serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>"; // url serveur de traduction serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php? isText=1&translationDirection={0}&stext={1}"; // dictionnaire des langues traduites Dictionary<string, string> languesTraduites = new Dictionary<string, string>(); languesTraduites["fe"]= "Franais-Anglais"; languesTraduites["fs"]= "Franais-Espagnol"; languesTraduites["ef"]= "Anglais-Franais"; serviceTraduction.LanguesTraduites = languesTraduites; // proxy //serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128"; // traduction string texte = "ce chien est perdu"; string deQuoiVersQuoi = "fe"; Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi)); texte = "l't sera chaud"; deQuoiVersQuoi = "fs"; Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi)); texte = "my tailor is rich"; deQuoiVersQuoi = "ef";

Programmation Internet

360

33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43.

Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi)); texte = "xx"; deQuoiVersQuoi = "ef"; Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi)); } catch (WebTraductionsException e) { // erreur Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code); } } } }

Les rsultats obtenus sont les suivants :


1. 2. 3. 4. Traduction Traduction Traduction Traduction [Franais-Anglais] de [ce chien est perdu] : [this dog is lost] [Franais-Espagnol] de [l't sera chaud] : [el verano ser caliente] [Anglais-Franais] de [my tailor is rich] : [mon tailleur est riche] [Anglais-Franais] de [xx] : [xx]

Le projet [dao] de la solution est compile en une DLL HttpTraductions.dll :

9.8.6

L'interface graphique de l'application

Revenons sur l'architecture de notre application :

utilisateur

Couche ui [ui]

[ServiceTraduction]

Couche [entites] SPRING

Internet

Serveur de traduction [trans.voila.fr]

Nous crivons maintenant la couche [ui]. Celle-ci fait l'objet du projet [ui] de la solution en construction :

3 1

Programmation Internet

361

Le dossier [lib] [3] contient certaines des DLL rfrences par le projet [4] : celles ncessaires Spring : Spring.Core, Common.Logging, antlr.runtime celle de la couche [dao] : HttpTraductions Le fichier [App.config] contient la configuration Spring :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. <?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> <sectionGroup name="spring"> <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" /> <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" /> </sectionGroup> </configSections> <spring> <context> <resource uri="config://spring/objects" /> </context> <objects xmlns="http://www.springframework.net"> <description>Traductions sur le web</description> <!-- le service de traduction --> <object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions"> <property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php? isText=1&amp;translationDirection={0}&amp;stext={1}"/> <!-<property name="ProxyHttp" value="pproxy.istia.uang:3128"/> --> <property name="RegexTraduction" value="&lt;div class=&quot;txtTrad&quot;&gt; (.*?)&lt;/div&gt;"/> <property name="LanguesTraduites"> <dictionary key-type="string" value-type="string"> <entry key="fe" value="Franais-Anglais"/> <entry key="ef" value="Anglais-Franais"/> ... <entry key="ei" value="Anglais-Italien"/> <entry key="ie" value="Italien-Anglais"/> </dictionary> </property> </object> </objects> </spring> </configuration>

ligne 15 : les objets instancier par Spring. Il n'y en aura qu'un, celui de la ligne 18 qui instancie le service de traduction avec la classe ServiceTraduction trouve dans la DLL HttpTraductions. ligne 19 : la proprit UrlServeurTraduction de la classe ServiceTraduction. Il y a une difficult avec le caractre & de l'Url. ce caractre a une signification dans un fichier Xml. Il doit donc tre protg. C'est le cas galement d'autres caractres que nous allons rencontrer dans la suite du fichier. Ils doivent tre remplacs par une squence [&code;] : & par [&amp;], < par [&lt;] > par [&gt;], " par [&quot;]. ligne 21 : la proprit ProxyHttp de la classe ServiceTraduction. Une proprit non initialise reste null. Ne pas dfinir cette proprit revient dire qu'il n'y a pas de proxy Http. ligne 23 : la proprit RegexTraduction de la classe ServiceTraduction. Dans l'expression rgulire, il a fallu remplacer les caractres [< > "] par leurs quivalents protgs. lignes 24-33 : la proprit LanguesTraduites de la classe ServiceTraduction.

Le programme [Program.cs] est excut au dmarrage de l'application. Son code est le suivant :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. using using using using using using System; System.Text; System.Windows.Forms; dao; Spring.Context; Spring.Context.Support;

namespace ui { static class Program { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main() {

Programmation Internet

362

15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61.

Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // --------------- Code dveloppeur // instanciation service de traduction IApplicationContext ctx = null; Exception ex = null; ServiceTraduction serviceTraduction = null; try { // contexte Spring ctx = ContextRegistry.GetContext(); // on demande une rfrence sur le service de traduction serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction; } catch (Exception e1) { // mmorisation exception ex = e1; } // formulaire afficher Form form = null; // y-a-t-il eu une exception ? if (ex != null) { // oui - on cre le message d'erreur afficher StringBuilder msgErreur = new StringBuilder(String.Format("Chane des exceptions : {0} {1}", "".PadLeft(40, '-'), Environment.NewLine)); Exception e = ex; while (e != null) { msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine)); msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine)); e = e.InnerException; } // cration fentre d'erreur laquelle on passe le message d'erreur afficher Form2 form2 = new Form2(); form2.MsgErreur = msgErreur.ToString(); // ce sera la fentre afficher form = form2; } else { // tout s'est bien pass // cration interface graphique [Form1] laquelle on passe la rfrence sur le sevice de traduction Form1 form1 = new Form1(); form1.ServiceTraduction = serviceTraduction; // ce sera la fentre afficher form = form1; } // affichage fentre Application.Run(form); } } }

Ce code a dj t utilis dans l'application Impts version 6, au paragraphe 5.6.2, page 203.

le service de traduction est cr ligne 27 par Spring. Si cette cration s'est bien passe, le formulaire [Form1] sera affich (lignes 52-55), sinon c'est le formulaire d'erreur [Form2] qui le sera (lignes 36-48).

Le formulaire [Form2] est celui utilis dans l'application Impts version 6 et a t expliqu au paragraphe 5.6.4, page 206. Le formulaire [Form1] est le suivant :

Programmation Internet

363

2 3

n type 1 TextBox 2 3 4 ComboBox Button TextBox

nom textBoxTexteATraduire comboBoxLangues buttonTraduire textBoxTraduction

rle bote de saisie du texte traduire MultiLine=true la liste des sens de traduction pour demander la traduction du texte [1] dans le sens [2] la traduction du texte [1]

Le code du formulaire [Form1] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. using using using using using System; System.Collections.Generic; System.Linq; System.Windows.Forms; dao;

namespace ui { public partial class Form1 : Form { // service de traduction public ServiceTraduction ServiceTraduction { get; set; } // dictionnaire des langues Dictionary<string, string> languesInverses = new Dictionary<string, string>(); // constructeur public Form1() { InitializeComponent(); } // chargement initial du formulaire private void Form1_Load(object sender, EventArgs e) { // construction du dictionnaire invers des langues foreach (string code in ServiceTraduction.LanguesTraduites.Keys) { // langues string langues = ServiceTraduction.LanguesTraduites[code]; // ajout (langues, code) au dictionnaire invers languesInverses[langues] = code; } // remplissage combo dans l'ordre alphabtique des langues string[] languesCombo = languesInverses.Keys.ToArray(); Array.Sort<string>(languesCombo); foreach (string langue in languesCombo) { comboBoxLangues.Items.Add(langue); } // slection 1re langue if (comboBoxLangues.Items.Count != 0) { comboBoxLangues.SelectedIndex = 0; } } private void buttonTraduire_Click(object sender, EventArgs e) { // qq chose a traduire ? string texte = textBoxTexteATraduire.Text.Trim(); if (texte == "") return;

Programmation Internet

364

44. 45. 46. 47. 48. 49. 50. 51. 52.

// traduction try { textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInverses[comboBoxLangues.SelectedItem.ToString()]); } catch (Exception ex) { textBoxTraduction.Text = ex.Message; } } } }

ligne 10 : une rfrence sur le service de traduction. Cette proprit publique a t initialise par [Program.cs], ligne 53. Lorsque les mthodes Form1_Load (ligne 20) ou buttonTraduire_Click (ligne 40) s'excutent, ce champ est donc dj initialis. ligne 12 : le dictionnaire des langues traduites avec des entres de type ["Franais-Anglais","fe"], c.a.d. l'inverse du dictionnaire LanguesTraduites rendu par le service de traduction. ligne 20 : la mthode Form1_Load s'excute au chargement du formulaire. lignes 22-27 : on utilise le dictionnaire serviceTraduction.LanguesTraduites ["fe","Franais-Anglais"] pour construire le dictionnaire languesInverses ["Franais-Anglais", "fe"]. ligne 29 : languesCombo est le tableau des cls du dictionnaire languesInverses, c.a.d. un tableau d'lments ["FranaisAnglais"] ligne 30 : ce tableau est tri afin de prsenter dans le combo les sens de traduction par ordre alphabtique lignes 31-33 : le combo des langues est rempli. ligne 40 : la mthode excute lorsque l'utilisateur clique sur le bouton [Traduire] ligne 46 : il suffit d'appeler la mthode serviceTraduction.Traduire pour demander la traduction. Le 1er paramtre est le texte traduire, le second le code du sens de traduction. Ce code est trouv dans le dictionnaire languesInverses partir de l'lment slectionn dans le combo des langues. ligne 48 : s'il y a une exception, elle est affiche la place de la traduction.

9.8.7

Conclusion

Cette application a montr que les clients web du framework .NET nous permettaient d'exploiter les ressources du web. La technique est chaque fois similaire :

dterminer l'Uri interroger. Cette Uri est la pluprt du temps paramtre. l'interroger trouver dans la rponse du serveur ce qu'on cherche grce des expressions rgulires

Cette technique est alatoire. En effet au fil du temps, l'Uri interroge ou l'expression rgulire permettant de trouver le rsultat attendu peuvent changer. On a donc intrt placer ces deux informations dans un fichier de configuration. Mais cela peut se rvler insuffisant. Nous verrons dans le chapitre suivant qu'il existe des ressources plus stables sur le web : les services web.

9.8.8

Un client SMTP (Simple Mail Tranport Protocol) avec la classe SmtpClient

Un client SMTP est un client d'un serveur SMTP, serveur d'envoi de courrier. La class .NET SmtpClient encapsule totalement les besoins d'un tel client. Le dveloppeur n'a pas connatre les dtails du protocole SMTP. Nous connaissons ce dernier. Il a t prsent au paragraphe 9.4.3, page 327. Nous prsentons la classe SmtpClient dans le cadre d'une application windows basique qui permet d'envoyer des courriers lectroniques avec pices jointes. L'application va se connecter au port 25 d'un serveur SMTP. On rappelle que sur la plupart des PC windows, les pare-feu ou autres antivirus bloquent les connexions vers le port 25. Il est alors ncessaire de dsactiver cette protection pour tester l'application :

Programmation Internet

365

Le client Smtp aura une architecture mono-couche :

utilisateur

Couche ui [ui] Internet

Serveur Smtp

Le projet Visual studio est le suivant :

L'interface graphique [SendMailForm.cs] de l'application est la suivante :

Programmation Internet

366

1 3 4 5 6

11 13 12

10

n 1 2 3 4 5 6

type TextBox NumericUpDown TextBox TextBox TextBox TextBox

nom textBoxServeur numericUpDownPort textBoxExpediteur textBoxTo textBoxCc textBoxBcc

7 8 9 10

Button ListBox TextBox TextBox

buttonAjouter listBoxPiecesJointes textBoxSujet textBoxMessage buttonEnvoyer textBoxRsultat buttonEffacer openFileDialog1

11 Button 12 TextBox 13 Button OpenfileDialog

rle nom du serveur SMTP auquel se connecter le port sur lequel se connecter adresse de l'expditeur du message adresses des destinataires sous la forme : adresse1,adresse2, ... adresses des destinataires en copie (CC=Carbon Copy) sous la forme : adresse1,adresse2, ... adresses des destinataires en copie aveugle (BCC=Blind Carbon Copy) sous la forme : adresse1,adresse2, ... Toutes les adresses de ces trois champs de saisie recevront le mme message avec les mmes attachements. Les personnes destinataires du message pourront connatre les adresses qui taient dans les champs 4 et 5 mais pas celles du champ 6. Le Bcc est donc une faon de mettre quelqu'un en copie sans que les autres destinataires du message le sachent. pour ajouter une pice jointe au courrier liste des pices joindre au courrier sujet du courrier le texte du message. MultiLine=true pour envoyer le message et les ventuelles pices jointes affiche un rsum du message envoy ou bien un message d'erreur si un problme a t rencontr pour effacer [12] contrle non visuel qui permet le choix d'une pice jointe dans le systme de fichiers local

Dans l'exemple prcdent, le rsum affich en [12] est le suivant :


Envoi russi...

Programmation Internet

367

Sujet : votre demande Destinataires : y2000@hotmail.com Cc : Bcc : Pices jointes : C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip Texte : Bonjour, Vous trouverez ci-joint le dossier de candidature l'ISTIA. Cordialement, ST

Le code du formulaire [SendMailForm.cs] est le suivant :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. using using using using using System; System.Windows.Forms; System.Net.Mail; System.Text.RegularExpressions; System.Text;

namespace Chap9 { public partial class SendMailForm : Form { public SendMailForm() { InitializeComponent(); } // ajout d'une pice jointe private void buttonAjouter_Click(object sender, EventArgs e) { // on paramtre la bote de dialogue openfileDialog1 openFileDialog1.InitialDirectory = Application.ExecutablePath; openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*"; openFileDialog1.FilterIndex = 0; openFileDialog1.FileName = ""; // on affiche la bote de dialogue et on rcupre son rsultat if (openFileDialog1.ShowDialog() == DialogResult.OK) { // on rcupre le nom du fichier listBoxPiecesJointes.Items.Add(openFileDialog1.FileName); } } private void textBoxServeur_TextChanged(object sender, EventArgs e) { setStatutEnvoyer(); }

private void setStatutEnvoyer() { buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != ""; 33. } 34. 35. // retirer une pice jointe 36. private void buttonRetirer_Click(object sender, EventArgs e) { 37. // pice jointe slectionne ? 38. if (listBoxPiecesJointes.SelectedIndex != -1) { 39. // on la retire 40. listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex); 41. // on met jour le bouton Retirer 42. buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0; 43. } 44. } 45. 46. private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) { 47. // pice jointe slectionne ? 48. if (listBoxPiecesJointes.SelectedIndex != -1) { 49. // on met jour le bouton Retirer 50. buttonRetirer.Enabled = true; 51. } 52. } 53. 54. // envoi du message avec ses attachements 55. private void buttonEnvoyer_Click(object sender, EventArgs e) { 56. .... 57. } 58. 59. private void textBoxTo_TextChanged(object sender, EventArgs e) { 60. setStatutEnvoyer(); 61. }

Programmation Internet

368

62. 63. 64. 65. 66. 67. 68. 69. 70. } 71. }

private void textBoxSujet_TextChanged(object sender, EventArgs e) { setStatutEnvoyer(); } private void buttonEffacer_Click(object sender, EventArgs e) { textBoxResultat.Text = ""; }

Nous ne commenterons pas ce code qui ne prsente pas de nouveauts. Pour comprendre la mthode buttonAjouter_Click de la ligne 14, le lecteur est invit relire le paragraphe 5.5.1, de la page 195. La mthode buttonEnvoyer_Click de la ligne 55, qui envoie le courrier est la suivante :
1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. private void buttonEnvoyer_Click(object sender, EventArgs e) { try { // sablier Cursor = Cursors.WaitCursor; // le client Smtp SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value); // le message MailMessage message = new MailMessage(); // expditeur message.Sender = new MailAddress(textBoxExpditeur.Text.Trim()); message.From = message.Sender; // destinataires Regex marqueur = new Regex("\\s*,\\s*"); string[] destinataires = marqueur.Split(textBoxTo.Text.Trim()); foreach (string destinataire in destinataires) { if (destinataire.Trim() != "") { message.To.Add(new MailAddress(destinataire)); } } // CC string[] copies = marqueur.Split(textBoxCc.Text.Trim()); foreach (string copie in copies) { if (copie.Trim() != "") { message.CC.Add(new MailAddress(copie)); } } // BCC string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim()); foreach (string blindCopie in blindCopies) { if (blindCopie.Trim() != "") { message.Bcc.Add(new MailAddress(blindCopie)); } } // sujet message.Subject = textBoxSujet.Text.Trim(); // texte du message message.Body = textBoxMessage.Text; // attachements foreach (string attachement in listBoxPiecesJointes.Items) { message.Attachments.Add(new Attachment(attachement)); } // envoi du message smtpClient.Send(message); // Ok - on affiche un rsum StringBuilder msg = new StringBuilder(String.Format("Envoi russi...{0}", Environment.NewLine)); msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine)); textBoxSujet.Clear(); msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine)); textBoxTo.Clear(); msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine)); textBoxCc.Clear(); msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine)); textBoxBcc.Clear(); msg.Append(String.Format("Pices jointes :{0}", Environment.NewLine)); foreach (string attachement in listBoxPiecesJointes.Items) { msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine)); }

Programmation Internet

369

58. 59. 60. 61. 62. 63. 64. 65. 66. 67.

msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine)); listBoxPiecesJointes.Items.Clear(); textBoxResultat.Text = msg.ToString(); } catch (Exception ex) { // on affiche l'erreur textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex); } // curseur normal Cursor = Cursors.Arrow; }

ligne 6 : le client Smtp est cr. Il a besoin de deux paramtres : le nom du serveur SMTP et le port sur lequel celui-ci opre ligne 8 : un message de type MailMessage est cr. C'est lui qui va encapsuler la totalit du message envoyer. ligne 10 : l'adresse lectronique Sender de l'expditeur est renseigne. Une adresse lectronique est une instance de type MailAddress construite patir d'une chane de caractres "xx@yy.zz". Il faut que cette chane ait la forme attendue pour une adresse lectronique, sinon une exception est lance. Dans ce cas, elle sera afiche dans le champ textBoxResultat (ligne 63) sous une forme peu conviviale. lignes 13-19 : les adresses lectroniques des destinataires sont places dans la liste To du messsage. On rcupre ces adresses dans le champ textBoxTo. L'expression rgulire de la ligne 13 permet de rcuprer les diffrentes adresses qui sont spares par une virgule. lignes 21-26 : on rpte le mme processus pour initialiser le champ CC du messsage avec les adresses en copie du champ textBoxCc. lignes 28-33 : on rpte le mme processus pour initialiser le champ Bcc du messsage avec les adresses en copie aveugle du champ textBoxBcc. ligne 35 : le champ Subject du messsage est initialis avec le sujet du champ textBoxSujet. ligne 37 : le champ Body du messsage est initialis avec le texte du message textBoxMessage. lignes 39-41 : les pices jointes sont attaches au message. Chaque pice jointe est ajoute sous forme d'un objet Attachment au champ Attachments du message. Un objet Attachment est instanci partir du chemin complet de la pice attacher dans le systme de fichiers local. ligne 43 : le message est envoy l'aide de la mthode Send du client Smtp. lignes 45-60 : criture du rsum de l'envoi dans le champ textBoxResultat et rinitialisation du formulaire. ligne 63 : affichage d'une ventuelle erreur

9.9
9.9.1

Un client Tcp gnrique asynchrone


Prsentation

Dans tous les exemples de ce chapitre, la communication client / serveur se faisait en mode bloquant qu'on appelle galement mode synchrone :

lorsqu'un client se connecte un serveur, il attend la rponse du serveur cette demande avant de continuer. lorsqu'un client lit une ligne de texte envoye par le serveur, il est bloqu tant que le serveur n'a pas envoy celle-ci. ct serveur, les threads de service qui assurent le service au client fonctionnent de la mme faon que ci-dessus.

Dans les interfaces graphiques, il est souvent ncessaire de ne pas bloquer l'utilisateur sur des oprations longues. Le cas souvent cit est celui du tlchargement d'un gros fichier. Pendant ce tlchargement, il faut laisser l'utilisateur libre de continuer interagir avec l'interface graphique. Nous nous proposons ici de rcrire le client Tcp gnrique du paragraphe 9.6.3, page 337 en y apportant les changements suivants :

l'interface sera graphique l'outil de communication avec le serveur sera un objet Socket le mode de communication sera asynchrone : le client initiera une connexion au serveur mais ne restera pas bloqu attendre qu'elle soit tablie le client initiera un envoi au serveur mais ne restera pas bloqu attendre qu'il soit termin le client initiera la rception de donnes provenant du serveur mais ne restera pas bloqu attendre la fin de celle-ci.

Rappelons quel niveau se situe l'objet Socket dans la communication client / serveur Tcp :

Programmation Internet

370

Application C

Application D

WebClient
Application A

SmtpClient Socket

Application B

TcpClient, TcpListener
Rseau

La classe Socket est celle qui opre le plus prs du rseau. Elle permet de grer finement la connexion rseau. Le terme socket dsigne une prise de courant. Le terme a t tendu pour dsigner une prise de rseau logicielle. Dans une communication TCP-IP entre deux machines A et B, ce sont deux sockets qui communiquent entre-eux. Une application peut travailler directement avec les sockets. C'est le cas de l'application A ci-dessus. Un socket peut tre un socket client ou serveur.

9.9.2

L'interface graphique du client Tcp asynchrone

L'application Visual studio est la suivante :

[ClientTcpAsynchrone.cs] est l'interface graphique. Celle-ci est la suivante :

1 2 5

4 3

6 8 7 9

n type nom 1 TextBox textBoxNomServeur 2 NumericUpDown numericUpDownPortServeur 3 RadioButton radioButtonLF radioButtonRCLF 4 Button buttonConnexion

rle nom du serveur Tcp auquel se connecter le port sur lequel se connecter pour indiquer la marque de fin de ligne que le client doit utiliser : LF "\n" ou RCLF "\r\n" pour se connecter sur le port [2] du serveur [1]. Le bouton a le libell [Connecter] lorsque le client n'est pas connect un serveur, [Dconnecter] lorsqu'il est connect.

Programmation Internet

371

n type 5 TextBox 6 7 8 4 ListBox ListBox Button Button

nom textBoxMsgToServeur listBoxEvts listBoxDialogue buttonRazEvts buttonRazDialogue

rle message envoyer au serveur une fois la connexion faite. Lorsque l'utilisateur tape sur la touche [Entre], le message est envoy avec la marque de fin de ligne choisie en [3] liste dans laquelle sont affichs les principaux vnements de la liaison client / serveur : connexion, dconnexion, fermeture de flux, erreurs de communication liste dans laquelle sont affichs les messages du dialogue client / serveur pour effacer la liste [6] pour effacer la liste [7]

Les principes de fonctionnent de cette interface sont les suivants :


l'utilisateur connecte son client Tcp graphique un service Tcp grce [1, 2, 3, 4]. un thread asynchrone accepte en continu toutes les donnes envoyes par le serveur Tcp et les affiche dans la liste [7]. Ce thread est dissoci des autres activits de l'interface. l'utilisateur peut envoyer son rythme des messages au serveur grce [5]. Chaque message est envoy par un thread asynchrone. A la diffrence du thread de rception qui ne s'arrte jamais, le thread d'mission est lui termin ds que le message a t envoy. Un nouveau thread asynchrone sera utilis pour le message suivant. la communication client / serveur se termine lorsque l'un des partenaires clt la connexion. L'utilisateur peut prendre cette initiative avec le bouton [4] qui une fois la connexion tablie a le libell [Dconnecter].

Voici une copie d'cran d'une excution :

4 1

en [1] : connexion un service POP en [2] : affichage des vnements ayant eu lieu lors de la connexion en [3] : le message envoy par le serveur POP l'issue de la connexion en [4] : le bouton [Connecter] est devenu le bouton [Dconnecter]

Programmation Internet

372

en [1], on a envoy la commande quit au serveur POP. Le serveur a rpondu +OK goodbye et a ferm la connexion en [2], cette fermeture ct serveur a t dtecte. Le client a alors ferm la connexion de son ct. en [3], le bouton [Dconnecter] est redevenu un bouton [Connecter]

9.9.3

Connexion asynchrone au serveur

L'appui sur le bouton [Connecter] provoque l'excution de la mthode suivante :


1. 2. 3. 4. 5. 6. 7. private void buttonConnexion_Click(object sender, EventArgs e) { // connexion ou dconnexion ? if (buttonConnexion.Text == "Dconnecter") dconnexion(); else connexion();

ligne 3 : le bouton peut avoir le libell [Connecter] ou [Dconnecter].

La mthode de connexion est la suivante :


1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. using System.Net.Sockets; ... namespace Chap9 { public partial class ClientTcp : Form { const int tailleBuffer = 1024; private Socket client = null; private byte[] data = new byte[tailleBuffer]; private string rponse = null; private string finLigne = "\r\n"; // dlgus public delegate void writeLog(string log); public ClientTcp() { InitializeComponent(); } .................................... private void connexion() { // vrifications donnes string nomServeur = textBoxNomServeur.Text.Trim(); if (nomServeur == "") { logEvent("indiquez le nom du serveur"); return; }

Programmation Internet

373

26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53.

// suivi logEvent(String.Format("connexion en cours au serveur {0}", nomServeur)); try { // cration socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // connexion asynchrone client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0], (int)numericUpDownPortServeur.Value, connect, client); } catch (Exception ex) { logEvent(String.Format("erreur de connexion : {0}", ex.Message)); return; } } // la connexion a eu lieu private void connect(IAsyncResult rsultat) { // on rcupre le socket du client Socket client = rsultat.AsyncState as Socket; ... } // suivi du processus private void logEvent(string msg) { .... } } }

ligne 1 : la classe Socket fait partie de l'espace de noms System.Net.Sockets.

Un certain nombre de donnes doivent tre partages entre plusieurs mthodes du formulaire. Ce sont les suivantes :

ligne 7 : client est le socket de communication avec le serveur lignes 6 et 8 : le client va recevoir ses messages dans un tableau d'octets data. ligne 9 : rponse est la rponse envoye par le serveur. ligne 10 : finLigne est la marque de fin de ligne utilise par le client Tcp - est