Académique Documents
Professionnel Documents
Culture Documents
Cours Csharp
Cours Csharp
Introduction
C# est un langage rcent. Il a t disponible en versions beta depuis lanne 2000 avant dtre officiellement disponible en fvrier
2002 en mme temps que la plate-forme .NET de Microsoft laquelle il est li. C# ne peut fonctionner quavec cet environnement
dexcution, environnement disponible pour le moment que sur les machines Windows NT, 2000 et XP.
Avec la plate-forme .NET, trois nouveaux langages sont apparus : C#, VB.VET, JSCRIPT.NET. C# est largement une copie de
Java. VB.NET et JSCRIPT.NET sont des extensions de Visual basic et Jscript pour la plate-forme .NET. Celle-ci rend disponible
aux programmes qui sexcutent en son sein un ensemble trs important de classes, classes trs proches de celles que lon trouve au
sein des machines virtuelles Java. 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 importantes :
la plate-forme .NET ne s'excute que sur les machines Windows alors que Java s'excute sur diffrents OS (windows,
unix, macintosh).
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.
De la mme faon que Java ne peut tre ignor, la plate-forme .NET ne peut l'tre, la fois cause du parc trs important de
machines windows installes et de l'effort fait par Microsoft pour la promouvoir et l'imposer. Il semble que C# soit un bon choix
pour dmarrer avec .NET, notamment pour les programmeurs Java, tellement ces deux langages sont proches. Ensuite on pourra
passer aisment de C# VB.NET ou un autre langage .NET. La syntaxe changera mais les classes .NET resteront les mmes.
Contrairement aux apparences, le passage de VB VB.NET est difficile. VB n'est pas un langage orient objets alors que VB.NET
l'est compltement. Le programmeur VB va donc tre confront des concepts qu'il ne matrise pas. Il parat plus simple
d'affronter ceux-ci avec un langage entirement nouveau tel que C# plutt qu'avec VB.NET o le programmeur VB aura toujours
tendance vouloir revenir ses habitudes VB.
Ce document n'est pas un cours exhaustif. Il est destin des gens connaissant dj la programmation et qui veulent dcouvrir C#.
Afin de faciliter la comparaison avec Java, il reprend la structure du document "Introduction au langage Java" du mme auteur.
Deux livres m'ont aid :
-
1.
1.1 INTRODUCTION
1.2 LES DONNEES DE C#
1.2.1 LES TYPES DE DONNEES PREDEFINIS
1.2.2 CONVERSION ENTRE TYPES SIMPLES ET TYPES OBJETS
1.2.3 NOTATION DES DONNEES LITTERALES
1.2.4 DECLARATION DES DONNEES
1.2.5 LES CONVERSIONS ENTRE NOMBRES ET CHAINES DE CARACTERES
1.2.6 LES TABLEAUX DE DONNEES
1.3 LES INSTRUCTIONS ELEMENTAIRES DE C#
1.3.1 ECRITURE SUR ECRAN
1.3.2 LECTURE DE DONNEES TAPEES AU CLAVIER
1.3.3 EXEMPLE D'ENTREES-SORTIES
1.3.4 REDIRECTION DES E/S
1.3.5 AFFECTATION DE LA VALEUR D'UNE EXPRESSION A UNE VARIABLE
1.4 LES INSTRUCTIONS DE CONTROLE DU DEROULEMENT DU PROGRAMME
1.4.1 ARRET
1.4.2 STRUCTURE DE CHOIX SIMPLE
1.4.3 STRUCTURE DE CAS
1.4.4 STRUCTURE DE REPETITION
1.5 LA STRUCTURE D'UN PROGRAMME C#
1.6 COMPILATION ET EXECUTION D'UN PROGRAMME C#
1.7 L'EXEMPLE IMPOTS
1.8 ARGUMENTS DU PROGRAMME PRINCIPAL
1.9 LES ENUMERATIONS
1.10 LA GESTION DES EXCEPTIONS
1.11 PASSAGE DE PARAMETRES A UNE FONCTION
1.11.1 PASSAGE PAR VALEUR
1.11.2 PASSAGE PAR REFERENCE
1.11.3 PASSAGE PAR REFERENCE AVEC LE MOT CLE OUT
7
7
7
8
8
8
9
10
12
12
13
13
13
14
20
20
20
21
21
24
24
24
26
27
28
31
31
31
32
2.
33
33
33
33
34
34
35
35
36
37
37
38
39
40
41
42
43
44
45
45
46
47
49
49
52
52
52
53
2.5
2.6
2.7
2.8
3.
LES STRUCTURES
LES INTERFACES
LES ESPACES DE NOMS
L'EXEMPLE IMPOTS
CLASSES .NET D'USAGE COURANT
3.1
3.1.1
3.2
3.2.1
3.2.2
3.3
3.4
3.5
3.6
3.7
3.8
3.9
3.9.1
3.9.2
3.9.3
3.9.4
3.9.5
3.10
4.
5.1
5.2
6.
WINCV
CHERCHER DE L'AIDE SUR LES CLASSES AVEC VS.NET
HELP/CONTENTS
HELP/INDEX
LA CLASSE STRING
LA CLASSE ARRAY
LA CLASSE ARRAYLIST
LA CLASSE HASHTABLE
LA CLASSE STREAMREADER
LA CLASSE STREAMWRITER
LA CLASSE REGEX
VERIFIER QU'UNE CHAINE CORRESPOND A UN MODELE DONNE
TROUVER TOUS LES ELEMENTS D'UNE CHAINE CORRESPONDANT A UN MODELE
RECUPERER DES PARTIES D'UN MODELE
UN PROGRAMME D'APPRENTISSAGE
LA METHODE SPLIT
LES CLASSES BINARYREADER ET BINARYWRITER
4.1
4.1.1
4.1.2
4.2
4.2.1
4.2.2
4.2.3
4.2.4
4.2.5
4.3
4.3.1
4.3.2
4.4
4.4.1
4.4.2
4.4.3
4.4.4
4.4.5
4.4.6
4.5
4.6
4.7
4.7.1
4.7.2
4.7.3
4.8
5.
GESTION D'EVENEMENTS
OBJETS DELEGATE
GESTION D'EVENEMENTS
ACCES AUX BASES DE DONNEES
55
58
61
62
66
66
66
69
69
72
73
75
77
79
81
82
83
85
86
87
88
89
90
93
93
93
94
97
97
98
100
100
102
102
107
108
108
108
109
110
112
114
115
117
119
124
124
129
131
133
136
136
137
142
6.1
6.2
6.3
6.3.1
6.3.2
6.3.3
6.3.4
6.3.5
6.4
7.
7.1
7.2
7.3
7.4
7.5
7.6
8.
GENERALITES
LES DEUX MODES D'EXPLOITATION D'UNE SOURCE DE DONNEES
ACCES AUX DONNEES EN MODE CONNECTE
LES BASES DE DONNEES DE L'EXEMPLE
UTILISATION D'UN PILOTE ODBC
UTILISATION D'UN PILOTE OLE DB
EXEMPLE 1 : MISE A JOUR D'UNE TABLE
EXEMPLE 2 : IMPOTS
ACCES AUX DONNEES EN MODE DECONNECTE
INTRODUCTION
CREATION DE THREADS D'EXECUTION
INTERET DES THREADS
ACCES A DES RESSOURCES PARTAGEES
ACCES EXCLUSIF A UNE RESSOURCE PARTAGEE
SYNCHRONISATION PAR EVENEMENTS
PROGRAMMATION TCP-IP
142
143
144
144
148
152
153
157
160
161
161
162
164
165
166
169
172
8.1 GENERALITES
8.1.1 LES PROTOCOLES DE L'INTERNET
8.1.2 LE MODELE OSI
8.1.3 LE MODELE TCP/IP
8.1.4 FONCTIONNEMENT DES PROTOCOLES DE L'INTERNET
8.1.5 LES PROBLEMES D'ADRESSAGE DANS L'INTERNET
8.1.6 LA COUCHE RESEAU DITE COUCHE IP DE L'INTERNET
8.1.7 LA COUCHE TRANSPORT : LES PROTOCOLES UDP ET TCP
8.1.8 LA COUCHE APPLICATIONS
8.1.9 CONCLUSION
8.2 GESTION DES ADRESSES RESEAU
8.3 PROGRAMMATION TCP-IP
8.3.1 GENERALITES
8.3.2 LES CARACTERISTIQUES DU PROTOCOLE TCP
8.3.3 LA RELATION CLIENT-SERVEUR
8.3.4 ARCHITECTURE D'UN CLIENT
8.3.5 ARCHITECTURE D'UN SERVEUR
8.3.6 LA CLASSE TCPCLIENT
8.3.7 LA CLASSE NETWORKSTREAM
8.3.8 ARCHITECTURE DE BASE D'UN CLIENT INTERNET
8.3.9 LA CLASSE TCPLISTENER
8.3.10 ARCHITECTURE DE BASE D'UN SERVEUR INTERNET
8.4 EXEMPLES
8.4.1 SERVEUR D'ECHO
8.4.2 UN CLIENT POUR LE SERVEUR D'ECHO
8.4.3 UN CLIENT TCP GENERIQUE
8.4.4 UN SERVEUR TCP GENERIQUE
8.4.5 UN CLIENT WEB
8.4.6 CLIENT WEB GERANT LES REDIRECTIONS
8.4.7 SERVEUR DE CALCUL D'IMPOTS
172
172
172
173
175
176
179
180
181
182
182
185
185
185
186
186
186
186
187
188
188
189
190
190
191
193
198
201
203
205
9.
210
SERVICES WEB
9.1
9.2
9.3
9.4
9.5
9.6
9.6.1
9.6.2
INTRODUCTION
UN PREMIER SERVICE WEB
UN CLIENT HTTP-GET
UN CLIENT HTTP-POST
UN CLIENT SOAP
ENCAPSULATION DES ECHANGES CLIENT-SERVEUR
LA CLASSE D'ENCAPSULATION
UN CLIENT CONSOLE
210
210
216
222
226
230
230
233
235
238
243
245
245
250
250
10.
253
A SUIVRE
des donnes
les instructions qui les manipulent
DONNEES
+--------------------
INSTRUCTIONS
+--------------------+
Codage
2 octets
4 octets
4 octets
8 octets
8 octets
1 octet
1 octet
2 octets
2 octets
4 octets
8 octets
16 octets
1 bit
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
rfrence d'objet
Domaine
caractre Unicode
[-231, 231-1] [2147483648, 2147483647]
[0, 232-1] [0, 4294967295]
[-263, 263 -1] [9223372036854775808, 9223372036854775807]
[0, 264 -1] [0, 18446744073709551615]
[-27 , 27 -1] [-128,+127]
[0 , 28 -1] [0,255]
[-215, 215-1] [-32768, 32767]
[0, 216-1] [0,65535]
[1.5 10-45, 3.4 10+38] en valeur absolue
[5.0 10-324, 1.7 10+308] en valeur absolue
[1.0 10-28,7.9 10+28] en valeur absolue avec 28 chiffres significatifs
true, false
char
chane de caractres
date et heure
int
long
byte
float
double
decimal
boolean
7
Int32
Int64
Decimal
Boolean
Char
Byte
Float
Double
Enum
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 remplace par un seul \ non interprt. 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.
La lecture du programme sera plus aise si l'on a donn la constante un nom significatif :
ex : 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 :
final 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.
"" + nombre
int.Parse(chaine) ou Int32.Parse
long.Parse(chaine) pu Int64.Parse
double.Parse(chane) ou Double.Parse(chane)
float.Parse(chane) ou 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 en C#. Cette erreur peut tre gre par la clause try/catch suivante :
try{
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 les
principales 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.
// espaces de noms imports
using System;
// la classe de test
public class conv1{
public static void Main(){
String S;
const int i=10;
const long l=100000;
const float f=45.78F;
double d=-14.98;
// nombre --> chane
S=""+i;
affiche(S);
S=""+l;
affiche(S);
S=""+f;
affiche(S);
Les bases de C#
S=""+d;
affiche(S);
//boolean --> chane
const bool b=false;
S=""+b;
affiche(S);
// chane --> int
int i1;
i1=int.Parse("10");
affiche(""+i1);
try{
i1=int.Parse("10.67");
affiche(""+i1);
} catch (Exception e){
affiche("Erreur "+e.Message);
}
// chane --> long
long l1;
l1=long.Parse("100");
affiche(""+l1);
try{
l1=long.Parse("10.675");
affiche(""+l1);
} catch (Exception e){
affiche("Erreur "+e.Message);
}
// chane --> double
double d1;
d1=double.Parse("100,87");
affiche(""+d1);
try{
d1=double.Parse("abcd");
affiche(""+d1);
} catch (Exception e){
affiche("Erreur "+e.Message);
}
// chane --> float
float f1;
f1=float.Parse("100,87");
affiche(""+f1);
try{
d1=float.Parse("abcd");
affiche(""+f1);
} catch (Exception e){
affiche("Erreur "+e.Message);
}
}// fin main
public static void affiche(String S){
Console.Out.WriteLine("S="+S);
}
}// fin classe
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;
10
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};
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}};
Le nombre d'lments dans chacune des dimensions peut tre obtenue par la mthode GetLenth(i) o i=0 reprsente la dimension
correspondant au 1er indice, i=1 la dimension correspondant au 2ime indice, 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
tableaux ne sont pas crs lors de la dclaration ci-dessus. L'exemple ci-dessous illustre la cration d'un tableau de tableaux :
// 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
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 :
// tableaux
using System;
// classe de test
public class test{
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["+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["+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
}//for i
// affichage
for (int i=0;i<noms.Length;i++){
for(int j=0;j<noms[i].Length;j++){
Les bases de C#
11
Console.Out.WriteLine("noms["+i+"]["+j+"]="+noms[i][j]);
}//for j
}//for i
}//Main
}//class
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
+-----------+--------+-->
+-----------+
+-------------------+
+-------+
4^
\
\ 5
+-------+
\ ---->
DISQUE
+-------+
12
Console.Error.WriteLine() crit sur le flux Error, habituellement associ lui aussi l'cran.
Les flux Out et Error sont associs par dfaut l'cran. Mais ils peuvent tre redirigs vers des fichiers texte au moment de l'excution
du programme comme nous le verrons prochainement.
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.
Les instructions
object obj=new object();
Console.Out.WriteLine(""+obj);
ont pour but de montrer que n'importe quel objet peut faire l'objet d'un affichage. Nous ne chercherons pas ici expliquer la
signification de ce qui est affich.
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 :
Les bases de C#
13
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 l'OS qui fixe la nature des priphriques 0,1 et 2. Considrons le programme suivant :
// imports
using System;
// redirections
public class console2{
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
L'excution prcdente ne redirige aucun des flux d'E/S standard In, Out, Error. Nos allons maintenant rediriger les trois flux. Le
flux In sera redirig vers un fichier in.txt, le flux Out vers le fichier out.txt, le flux Error vers le fichier error.txt. Cette redirection a lieu
sur la ligne de commande sous la forme
E:\data\serge\MSNET\c#\bases\1>console2 0<in.txt 1>out.txt 2>error.txt
On voit clairement que les flux Out et In n'crivent pas sur les mmes priphriques.
14
L'opration variable=expression;
est elle-mme une expression dont l'valuation se droule de la faon suivante :
V1=V2=expression
addition
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.
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 :
Les bases de C#
15
double x, y=4;
x=Math.Sqrt(y);
La dfinition complte de la classe Math est la suivante :
// from module 'c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll'
public sealed class Math :
object
{
// Fields
public static const double E;
public static const double PI;
// Constructors
// Methods
public static long Abs(long value);
public static int Abs(int value);
public static short Abs(short value);
public static SByte Abs(SByte value);
public static double Abs(double value);
public static Decimal Abs(Decimal value);
public static float Abs(float value);
public static double Acos(double d);
public static double Asin(double d);
public static double Atan(double d);
public static double Atan2(double y, double x);
public static double Ceiling(double a);
public static double Cos(double d);
public static double Cosh(double value);
public virtual bool Equals(object obj);
public static double Exp(double d);
public static double Floor(double d);
public virtual int GetHashCode();
public Type GetType();
public static double IEEERemainder(double x, double y);
public static double Log(double a, double newBase);
public static double Log(double d);
public static double Log10(double d);
public static Decimal Max(Decimal val1, Decimal val2);
public static byte Max(byte val1, byte val2);
public static short Max(short val1, short val2);
public static UInt32 Max(UInt32 val1, UInt32 val2);
public static UInt64 Max(UInt64 val1, UInt64 val2);
public static long Max(long val1, long val2);
public static int Max(int val1, int val2);
public static double Max(double val1, double val2);
public static float Max(float val1, float val2);
public static UInt16 Max(UInt16 val1, UInt16 val2);
public static SByte Max(SByte val1, SByte val2);
public static int Min(int val1, int val2);
public static UInt32 Min(UInt32 val1, UInt32 val2);
public static short Min(short val1, short val2);
public static UInt16 Min(UInt16 val1, UInt16 val2);
public static long Min(long val1, long val2);
public static double Min(double val1, double val2);
public static Decimal Min(Decimal val1, Decimal val2);
public static UInt64 Min(UInt64 val1, UInt64 val2);
public static float Min(float val1, float val2);
public static byte Min(byte val1, byte val2);
public static SByte Min(SByte val1, SByte val2);
public static double Pow(double x, double y);
public static double Round(double a);
public static Decimal Round(Decimal d);
public static Decimal Round(Decimal d, int decimals);
public static double Round(double value, int digits);
public static int Sign(SByte value);
public static int Sign(short value);
public static int Sign(int value);
public static int Sign(long value);
public static int Sign(Decimal value);
public static int Sign(double value);
public static int Sign(float value);
public static double Sin(double a);
public static double Sinh(double value);
public static double Sqrt(double d);
public static double Tan(double a);
public static double Tanh(double value);
public virtual string ToString();
} // end of System.Math
16
17
-1
La variable egal aura la valeur true si les deux chanes sont gales.
Les oprateurs relationnels ont priorit sur les oprateurs && et ||.
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
int i=0x123F, k=0xF123;
uint j=0xF123;
opration
i<<4
i>>4
k>>4
i&j
i|j
~i
valeur
0x23F0
0x0123 le bit de signe est prserv.
0xFF12 le bit de signe est prserv.
0x1023
0xF33F
0xEDC0
18
1.3.5.9 L'oprateur ?
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. 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. 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.
gd
dg
dg
gd
gd
gd
gd
gd
gd
gd
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.
// priorit de () sur /
Ici il est ncessaire de changer le type de i ou j en rel sinon la division donnera le quotient entier et non rel.
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.
Les bases de C#
19
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 DOS dans la variable systme ERRORLEVEL dont la valeur peut tre teste dans un
fichier batch. Sous Unix, c'est la variable $? qui rcupre la valeur de status.
Environment.Exit(0);
exemple
if (x>0)
if(n>1)
if(n>6)
Console.Out.WriteLine(">6");
else Console.Out.WriteLine ("<=6");
Dans l'exemple prcdent, le else se rapporte quel if ? La rgle est qu'un else se rapporte toujours au if le plus proche : if(n>6) dans
l'exemple. Considrons un autre exemple :
Les bases de C#
20
if(n>1)
if(n>6)
Console.Out.WriteLine (">6");
else;
// else du if(n>6) : rien faire
else Console.Out.WriteLine ("<=1"); // else du if(n>1)
Ici nous voulions mettre un else au if(n>1) et pas de else au if(n>6). A cause de la remarque prcdente, nous sommes obligs de
mettre un else au if(n>6), dans lequel il n'y a aucune instruction.
notes
En C#
int choix=0; bool erreur=false;
switch(choix){
case 0: Environment.Exit(0);
case 1: M1();break;
case 2: M2();break;
default: erreur=true;break;
}
21
Structure for
La syntaxe est la suivante :
for (i=id;i<=if
if;i=i+ip){
if
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.
Notes
expression est une collection d'objets. La collection d'objets 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.
afficherait :
paul
hlne
jacques
sylvie
22
actions;
On boucle tant que la condition est vrifie. La boucle peut ne jamais tre excute.
notes:
On boucle jusqu' ce que la condition devienne fausse ou tant que la condition est vraie. Ici la boucle est faite au moins une fois.
notes
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.
23
Exemples
Les programmes suivants calculent tous la somme des n premiers nombres entiers.
1
2
for(i=1,
somme=0;i<=n;i=i+1)
for
somme=somme+a[i];
for (i=1, somme=0;i<=n;somme=somme+a[i], i=i+1);
3 i=1;somme=0;
while(i<=n)
while
{ somme+=i; i++; }
4 i=1; somme=0;
do somme+=i++;
while (i<=n);
Si on utilise des fonctions susceptibles de gnrer des exceptions qu'on ne souhaite pas grer finement, on pourra encadrer le code
du programme par une clause try/catch :
public class test1{
public static void Main(){
try{
code du programme
} catch (Exception e){
// gestion de l'erreur
}// try
}// main
}// class
Les bases de C#
24
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 :
12620.0
13190
15640
24740
31810
39970
48360
55790
92970
127860
151250
172040
195000
0
0
0.05
0.1
0.15
0.2
0.25
0.3
0.35
0.4
0.45
0.50
0.55
0.60
0.65
0
631
1290.5
2072.5
3309.5
4900
6898.5
9316.5
12106
16754.5
23147.5
30710
39312
49062
Chaque ligne a 3 champs. Pour calculer l'impt I, on recherche la premire ligne o QF<=champ1. Par exemple, si QF=23000 on
trouvera la ligne
24740 0.15 2072.5
L'impt I est alors gal 0.15*R - 2072.5*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.65 49062
ce qui donne l'impt I=0.65*R - 49062*nbParts.
Le programme C# correspondant est le suivant :
using System;
public class impots{
// ------------ main
public static void Main(){
// tableaux de donnes ncessaires au calcul de l'impt
decimal[] Limites=new decimal[]
{12620M,13190M,15640M,24740M,31810M,39970M,48360M,55790M,92970M,127860M,151250M,172040M,195000M,0M};
decimal[] CoeffN=new decimal[]
{0M,631M,1290.5M,2072.5M,3309.5M,4900M,6898.5M,9316.5M,12106M,16754.5M,23147.5M,30710M,39312M,49062M};
// on rcupre le statut marital
bool OK=false;
string reponse=null;
while(! OK){
Console.Out.Write("Etes-vous mari(e) (O/N) ? ");
reponse=Console.In.ReadLine().Trim().ToLower();
if (reponse!="o" && reponse!="n")
Console.Error.WriteLine("Rponse incorrecte. Recommencez");
else OK=true;
}//while
bool Marie = reponse=="o";
// nombre d'enfants
OK=false;
int NbEnfants=0;
while(! OK){
Console.Out.Write("Nombre d'enfants : ");
reponse=Console.In.ReadLine();
try{
NbEnfants=int.Parse(reponse);
if(NbEnfants>=0) OK=true;
else Console.Error.WriteLine("Rponse incorrecte. Recommencez");
} catch(Exception){
Console.Error.WriteLine("Rponse incorrecte. Recommencez");
}// try
}// while
// salaire
OK=false;
int Salaire=0;
while(! OK){
Console.Out.Write("Salaire annuel : ");
reponse=Console.In.ReadLine();
try{
Salaire=int.Parse(reponse);
if(Salaire>=0) OK=true;
else Console.Error.WriteLine("Rponse incorrecte. Recommencez");
Les bases de C#
25
} catch(Exception){
Console.Error.WriteLine("Rponse incorrecte. Recommencez");
}// try
}// 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;
Revenu=0.72M*Salaire;
// quotient familial
decimal QF;
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)(i*0.05M*Revenu-CoeffN[i]*NbParts);
// on affiche le rsultat
Console.Out.WriteLine("Impt payer : " + impots);
}// main
}// classe
Il faut noter que impots.exe n'est pas directement excutable par le processeur mais uniquement. Il contient en ralit du code
intermdiaire qui n'est excutable que sur une plate-forme .NET. Les rsultats obtenus sont les suivants :
E:\data\serge\MSNET\c#\impots\4>impots
Etes-vous mari(e) (O/N) ? o
Nombre d'enfants : 3
Salaire annuel : 200000
Impt payer : 16400
E:\data\serge\MSNET\c#\impots\4>impots
Etes-vous mari(e) (O/N) ? n
Nombre d'enfants : 2
Salaire annuel : 200000
Impt payer : 33388
E:\data\serge\MSNET\c#\impots\4>impots
Etes-vous mari(e) (O/N) ? w
Rponse incorrecte. Recommencez
Etes-vous mari(e) (O/N) ? q
Rponse incorrecte. Recommencez
Etes-vous mari(e) (O/N) ? o
Nombre d'enfants : q
Rponse incorrecte. Recommencez
Nombre d'enfants : 2
Salaire annuel : q
Rponse incorrecte. Recommencez
Salaire annuel : 1
Impt payer : 0
26
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 :
P arg0 arg1 argn
et si la fonction Main est dclare comme suit :
public static void main(String[] arg);
on aura arg[0]="arg0", arg[1]="arg1" Voici un exemple :
// imports
using System;
public class arg1{
public static void Main(String[] args){
// on liste les paramtres
Console.Out.WriteLine("Il y a " + args.Length + " arguments");
for (int i=0;i<args.Length;i++){
Console.Out.WriteLine("arguments["+i+"]="+args[i]);
}
}//Main
}//classe
le paramtre args est un tableau de chanes de caractres qui reoit les arguments passs sur la ligne de commande lors de l'appel du
programme.
args.Length est le nombre d'lements du tableau args
args[i] est l'lment i du tableau
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 :
// une variable qui prend ses valeurs dans l'numration mentions
mention maMention=mention.Passable;
27
Console.Out.WriteLine(m);
}//foreach
De la mme faon que le type simple int est quivalent la classe Int32, le type simple enum est quivalent la classe Enum. Cette
classe 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'information sur le type d'une donne. Le type d'une variable v est
obtenu par v.GetType(). 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
foreach(int m in Enum.GetValues(maMention.GetType())){
Console.Out.WriteLine(m);
}//foreach
on obtiendra la liste des valeurs de l'numration sous forme d'entiers. C'est ce que montre le programme suivant :
// numration
using System;
public class intro{
// une numration
enum mention {Passable,AssezBien,Bien,TrsBien, Excellent};
public static void Main(){
// une variable qui prend ses valeurs dans l'numration mentions
mention maMention=mention.Passable;
// affichage valeur variable
Console.Out.WriteLine("mention="+maMention);
// test avec valeur de l'numration
if(maMention==mention.Passable){
Console.Out.WriteLine("Peut mieux faire");
}//if
// liste des mentions
foreach(mention m in Enum.GetValues(maMention.GetType())){
Console.Out.WriteLine(m);
}//foreach
foreach(int m in Enum.GetValues(maMention.GetType())){
Console.Out.WriteLine(m);
}//foreach
}//Main
}//classe
Les bases de C#
28
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 driv du type Exception. On peut tre plus prcis en utilisant des types tels que IOException, 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{
appel de la fonction susceptible de gnrer l'exception
} catch (IOException e){
traiter l'exception e
}
} catch (SystemException e){
traiter l'exception e
}
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 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.Error.WriteLine("L'erreur suivante s'est produite : "+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.Error.WriteLine("L'erreur suivante s'est produite : "+ex.ToString());
...
}//catch
On peut crire aussi :
catch (Exception ex){
Console.Error.WriteLine("L'erreur suivante s'est produite : "+ex);
...
}//catch
Nous avons ici une opration string + Exception qui va tre automatiquement transforme en string + Exception.ToString() par le
compilateur afin de faire la concatnation de deux chanes de caractres.
L'exemple suivant montre une exception gnre par l'utilisation d'un lment de tableau inexistant :
// tableaux
// imports
using System;
public class tab1{
public static void Main(String[] args){
// dclaration & initialisation d'un tableau
Les bases de C#
29
Voici un autre exemple o on gre l'exception provoque par l'affectation d'une chane de caractres un nombre lorsque la chane
ne reprsente pas un nombre :
// imports
using System;
public class console1{
public static void Main(String[] args){
// On demande le nom
System.Console.Write("Nom : ");
// lecture rponse
String nom=System.Console.ReadLine();
// on demande l'ge
int age=0;
Boolean ageOK=false;
while ( ! ageOK){
// question
Console.Out.Write("ge : ");
// lecture-vrification rponse
try{
age=int.Parse(System.Console.ReadLine());
ageOK=true;
}catch {
Console.Error.WriteLine("Age incorrect, recommencez...");
}//try-catch
}//while
// affichage final
Console.Out.WriteLine("Vous vous appelez " + nom + " et vous avez " + age + " ans");
Console.ReadLine();
}//Main
}//classe
E:\data\serge\MSNET\c#\bases\1>console1
Nom : dupont
ge : xx
Age incorrect, recommencez...
ge : 12
Vous vous appelez dupont et vous avez 12 ans
Les bases de C#
30
}
Dans la dfinition de la fonction, 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 utlisation de cette fonction :
public static void Main(){
int age=20;
changeInt(age);
Console.Out.WriteLine("Paramtre effectif age="+age);
}
Ici dans l'instruction 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.
La valeur 20 du paramtre effectif a t recopie dans le paramtre formel a. Celui-ci a t ensuite modifi. Le paramtre effectif est
lui rest inchang. Ce mode de passage convient aux paramtres d'entre d'une fonction.
31
Le paramtre effectif a suivi la modification du paramtre formel. Ce mode de passage convient aux paramtres de sortie d'une
fonction.
On peut contourner l'obstacle en affectant une valeur initiale age. 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 :
// passage de paramtres par valeur une fonction
using System;
public class param2{
public static void Main(){
int age=20;
changeInt(out age);
Console.Out.WriteLine("Paramtre effectif age="+age);
}
private static void changeInt(out int a){
a=30;
Console.Out.WriteLine("Paramtre formel a="+a);
}
}
Les bases de C#
32
}
type4 m4(){ // mthode
p1
p2
m3
m4
A partir de la classe C1 prcdente, on peut crer de nombreux objets O1, O2, Tous auront les proprits p1, p2, et les
mthodes m3, m4, Mais ils auront des valeurs diffrentes pour leurs proprits pi ayant ainsi chacun un tat qui leur est propre.
Par analogie la dclaration
int i,j;
cre deux objets (le terme est incorrect ici) de type (classe) int. Leur seule proprit est leur valeur.
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.
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 classes. 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 :
Classes, Structures, Interfaces
33
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 fonction 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)
// mthode
public void identifie(){
Console.Out.WriteLine(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);
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 :
Classes, Structures, Interfaces
34
personne p1=null;
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.
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 :
public void initialise(string P, string N, int age){
prenom=P;
nom=N;
this.age=age;
this
}
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;
this
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.
35
personne.cs
using System;
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.Out.WriteLine(prenom+","+nom+","+age);
}
}// classe personne
La compilation a produit un fichier personne.dll. C'est l'option de compilation /t:library qui indique de produire un fichier "assembly".
Maintenant compilons le fichier test1.cs :
Classes, Structures, Interfaces
36
L'option de compilation /r:personne.dll indique au compilateur qu'il trouvera certaines classes dans le fichier personne.dll. Lorsque dans
le fichier source test1.cs, il trouvera une rfrence la classe personne classe non dclare dans le source test1.cs, il cherchera la classe
personne dans les fichiers .dll rfrencs par l'option /r. Il trouvera ici la classe personne dans l'assemblage personne.dll. On aurait pu
mettre dans cet assemblage d'autres classes. Pour utiliser lors de la compilation plusieurs fichiers de classes compiles, on crira :
csc /r:fic1.dll /r:fic2.dll ... fichierSource.cs
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, celle-ci ayant t compile dans personne.dll comme il a t expliqu prcdemment :
using System;
public class test1{
public static void Main(){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
Console.Out.Write("p1=");
p1.identifie();
personne p2=new personne();
p2.initialise(p1);
Console.Out.Write("p2=");
p2.identifie();
}
}
et ses rsultats :
p1=Jean,Dupont,30
p2=Jean,Dupont,30
37
classe objet;
// mthode
public void identifie(){
Console.Out.WriteLine(prenom+","+nom+","+age);
}
Nos deux constructeurs se contentent de faire appel aux mthodes initialise correspondantes. 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 :
using System;
public class test1{
public static void Main(){
personne p1=new personne("Jean","Dupont",30);
Console.Out.Write("p1=");
p1.identifie();
personne p2=new personne(p1);
Console.Out.Write("p2=");
p2.identifie();
}
}
38
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 celui rfrenc par p2.
Lorsqu'on crit :
personne p3=new personne(p1);
il y a cration d'un nouvel objet, copie de l'objet rfrenc par p1. 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.
39
40
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 suivants :
P=(Jean,Michelin,34)
P=(Jean,Michelin,56)
Une proprit permet de lire (get) ou de fixer (set) la valeur d'un attribut. Dans notre exemple, nous avons prfix les noms des
attributs du signe _ afin que les proprits portent le nom des attributs primitifs. En effet, une proprit ne peut porter le mme
nom que l'attribut qu'elle gre car alors il y a un conflit de noms dans la classe. Nous avons donc appel nos attributs _prenom, _nom,
_age et modifi les constructeurs et mthodes en consquence. Nous avons ensuite cr trois proprits nom, prenom et age. Une
proprit est dclare comme suit :
public type proprit{
get {...}
set {...}
}
Classes, Structures, Interfaces
41
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 pour l'ge.
Comment ces mthodes get et set sont-elles appeles ? Considrons le programme de test suivant :
using System;
public class test1{
public static void Main(){
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
}//Main
}//classe
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
P.age=56;
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 :
P=(Jean,Michelin,34)
P=(Jean,Michelin,56)
ge (-4) invalide
Les proprits nous permettent donc de manipuler des attributs privs comme s'ils taient publics.
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 n'est pas l'attribut d'un objet particulier, il est l'attribut de toute une classe. Aussi
a-t-on besoin d'une proprit dclare elle-aussi static :
// proprit de classe
public static long nbPersonnes{
get{ return _nbPersonnes;}
}//nbPersonnes
42
using System;
public class personne{
// attributs de classe
private static long _nbPersonnes=0;
// attributs d'instance
private String _prenom;
private String _nom;
private int _age;
// constructeurs
public personne(String P, String N, int age){
// une personne de plus
_nbPersonnes++;
this._prenom=P;
this._nom=N;
this._age=age;
}
public personne(personne P){
// une personne de plus
_nbPersonnes++;
this._prenom=P._prenom;
this._nom=P._nom;
this._age=P._age;
}
// identifie
public void identifie(){
Console.Out.WriteLine(_prenom+","+_nom+","+_age);
}
// proprit de classe
public static long nbPersonnes{
get{ return _nbPersonnes;}
}//nbPersonnes
// proprits d'instance
public string prenom{
get { return _prenom; }
set { _prenom=value; }
}//prenom
public string nom{
get { return _nom; }
set { _nom=value; }
}//nom
public int age{
get { return _age; }
set {
// age valide ?
if(value>=0){
_age=value;
} else
throw new Exception("ge ("+value+") invalide");
}//if
}//age
}//classe
43
Nous avons dj dit que par dfaut C# passait les paramtres effectifs d'une fonction par valeur : les valeurs des paramtres
effectifs sont recopies dans les paramtres formels. Dans le cas d'un objet, il ne faut pas se laisser tromper par l'abus de langage qui
est fait systmatiquement en parlant d'objet au lieu de rfrence d'objet. Un objet n'est manipul que via une rfrence (un pointeur)
sur lui. Ce qui est donc transmis une fonction, n'est pas l'objet lui-mme mais une rfrence sur cet objet. C'est donc la valeur de
la rfrence et non la valeur de l'objet lui-mme qui est duplique dans le paramtre formel : il n'y a pas construction d'un nouvel
objet.
Si une rfrence d'objet R1 est transmise une fonction, elle sera recopie dans le paramtre formel correspondant R2. Aussi les
rfrences R2 et R1 dsignent-elles le mme objet. Si la fonction modifie l'objet point par R2, elle modifie videmment celui
rfrenc par R1 puisque c'est le mme.
R1
objet
Recopie
R2
La mthode modifie est dclare static parce que c'est une mthode de classe : on n'a pas la prfixer par un objet pour l'appeler.
Les rsultats obtenus sont les suivants :
Construction personne(string, string,
Paramtre effectif avant modification
Paramtre formel avant modification :
Paramtre formel aprs modification :
Paramtre effectif aprs modification
int)
: Jean,Dupont,30
Jean,Dupont,30
Sylvie,Vartan,52
: Sylvie,Vartan,52
On voit qu'il n'y a construction que d'un objet : celui de la personne p1 de la fonction Main et que l'objet a bien t modifi par la
fonction modifie.
44
L'instruction personne[] amis=new personne[3]; 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.
On obtient les rsultats suivants :
Construction personne(string, string, int)
Construction personne(string, string, int)
Construction personne(string, string, int)
---------------Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66
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 :
using System;
public class personne{
// attributs de classe
private static long _nbPersonnes=0;
// attributs d'instance
private String _prenom;
private String _nom;
private int _age;
// constructeurs
public personne(String P, String N, int age){
// une personne de plus
_nbPersonnes++;
// construction
this._prenom=P;
this._nom=N;
this._age=age;
// suivi
Console.Out.WriteLine("Construction personne(string, string, int)");
}
public personne(personne P){
// une personne de plus
Classes, Structures, Interfaces
45
_nbPersonnes++;
// construction
this._prenom=P._prenom;
this._nom=P._nom;
this._age=P._age;
// suivi
Console.Out.WriteLine("Construction personne(string, string, int)");
// proprit de classe
public static long nbPersonnes{
get{ return _nbPersonnes;}
}//nbPersonnes
// proprits d'instance
public string prenom{
get { return _prenom; }
set { _prenom=value; }
}//prenom
public string nom{
get { return _nom; }
set { _nom=value; }
}//nom
public int age{
get { return _age; }
set {
// age valide ?
if(value>=0){
_age=value;
} else
throw new Exception("ge ("+value+") invalide");
}//if
}//age
public string identite{
get { return "personne("+_prenom+","+_nom+","+age+")";}
}
}//classe
La mthode identifie a t remplace par la proprit identit en lecture seule et qui identifie la personne. Nous crons une classe
enseignant hritant de la classe personne :
using System;
public class enseignant : personne {
// attributs
private int _section;
// constructeur
public enseignant(string P, string N, int age,int section) : base(P,N,age) {
this._section=section;
// suivi
Console.Out.WriteLine("Construction enseignant(string,string,int,int)");
}//constructeur
// proprit section
public int section{
get { return _section; }
set { _section=value; }
}// section
}//classe
un attribut section qui est le n de section auquel appartient l'enseignant dans le corps des enseignants (une section par
discipline en gros)
un nouveau constructeur permettant d'initialiser tous les attributs d'un enseignant
La dclaration
46
this._section=section;
}//constructeur
La dclaration
public enseignant(String P, String N, int age,int section) : base(P,N,age) {
dclare que le constructeur reoit quatre paramtres P, N, age, section et en passe trois (P,N,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 les
paramtres passss (P,N,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 :
this.s_ection=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).
Compilons les classes personne et enseignant dans des assemblages :
E:\data\serge\MSNET\c#\objetsPoly\12>csc /t:library personne.cs
E:\data\serge\MSNET\c#\objetsPoly\12>csc /r:personne.dll /t:library enseignant.cs
E:\data\serge\MSNET\c#\objetsPoly\12>dir
26/04/2002 16:15
1 341 personne.cs
26/04/2002 16:30
4 096 personne.dll
26/04/2002 16:32
345 enseignant.cs
26/04/2002 16:32
3 072 enseignant.dll
On remarquera que pour compiler la classe fille enseignant, il a fallu rfrencer le fichier personne.dll qui contient la classe personne.
Tentons un premier programme de test :
using System;
public class test1{
public static void Main(){
Console.Out.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 identit mais sa
classe parent en a une qui de plus est publique : elle devient par hritage une mthode publique de la classe enseignant. Les rsultats
obtenus sont les suivants :
Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
personne(Jean,Dupont,30)
On voit que :
" un objet personne a t construit avant l'objet enseignant
" l'identit obtenue est celle de l'objet personne
47
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 :
using System;
public class enseignant : personne {
// attributs
private int _section;
// constructeur
public enseignant(string P, string N, int age,int section) : base(P,N,age) {
this._section=section;
// suivi
Console.Out.WriteLine("Construction enseignant(string,string,int,int)");
}//constructeur
// proprit section
public int section{
get { return _section; }
set { _section=value; }
}// section
// surcharge proprit identit
public new string identite{
get { return "enseignant("+base.identite+","+_section+")"; }
}//proprit identit
}//classe
La mthode identite de la classe enseignant s'appuie sur la mthode identite de sa classe mre (base.identite) 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 :
enseignant
personne
identite
identite
La proprit identit 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{
48
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 Ci 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){
.
}
que
enseignant e;
...
affiche(e);
Dans ce dernier cas, le paramtre formel de type personne de la fonction affiche va recevoir une valeur de type enseignant. Comme le
type enseignant drive du type personne, c'est lgal.
La mthode p.identite rend une chane de caractres identifiant l'objet personne. Que se passe-t-il dans le cas de notre exemple
prcdent dans le cas d'un objet enseignant :
enseignant e=new enseignant(...);
affiche(e);
49
}
// affiche
public static void affiche(personne p){
// affiche identit de p
Console.Out.WriteLine(p.identite);
}//affiche
}
L'excution montre que l'instruction p.identite a excut chaque fois la proprit identite d'une personne, la personne contenue dans
l'enseignant e, puis 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 point par p.
Il est possible d'obtenir ce rsultat en dclarant identite comme une proprit virtuelle (virtual) dans la classe de base personne :
public virtual string identite{
get { return "personne("+_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 utiliser alors le mot cl override au lieu de new pour qualifier leur
proprit/mthode redfinie. Ainsi dans la classe enseignant, la proprit identite est dfinie comme suit :
// surcharge proprit identit
public override string identite{
get { return "enseignant("+base.identite+","+_section+")"; }
}//proprit identit
Le programme de test :
using System;
public class test1{
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.Out.WriteLine(p.identite);
}//affiche
}
Cette fois-ci, on a bien eu l'identit complte de l'enseignant. Surchargeons maintenant une mthode plutt qu'une proprit. La
classe object est la classe "mre" de toutes les classes C#. Ainsi lorsqu'on crit :
public class personne
on crit implicitement :
public class personne : object
50
// Constructors
public Object();
// Methods
public virtual bool Equals(object obj);
public static bool Equals(object objA, object objB);
public virtual int GetHashCode();
public Type GetType();
public static bool ReferenceEquals(object objA, object objB);
public virtual string ToString();
} // end of System.Object
La mthode ToString rend le nom de la classe laquelle appartient l'objet comme le montre l'exemple suivant :
using System;
public class test1{
public static void Main(){
// un enseignant
Console.Out.WriteLine(new enseignant("Lucile","Dumas",56,61).ToString());
// une personne
Console.Out.WriteLine(new personne("Jean","Dupont",30).ToString());
}
}
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 est quand mme capable d'afficher le nom rel de la classe de l'objet.
Redfinissons la mthode ToString dans les classes personne et enseignant :
// ToString
public override string ToString(){
// on rend la proprit identite
return identite;
}//ToString
La dfinition est la mme dans les deux classes. Considrons le programme de test suivant :
using System;
public class test1{
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.Out.WriteLine(""+p);
}//affiche
}
Attardons-nous sur la mthode affiche qui admet pour paramtre une personne p. Que signifie l'expression ""+p ? Le compilateur va
ici chercher transformet l'objet p en string et cherche toujours pour cela l'existence d'une mthode appele ToSTring. Donc ""+p
devient ""+p.Tostring(). La mthode ToString tant virtuelle, le compilateur va excuter la mthode ToString de l'objet rellement
point par p. C'est ce que montrent les rsultats d'excution :
Construction personne(string, string, int)
Construction enseignant(string,string,int,int)
enseignant(personne(Lucile,Dumas,56),61)
Construction personne(string, string, int)
personne(Jean,Dupont,30)
51
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
2.3.2 Un exemple
On cre une classe listeDePersonnes drive de la classe ArrayList. Cette classe implmente un tableau dynamique et est prsente
dans le chapitre qui suit. De cette classe, nous n'utilisons que les lments suivants :
la mthode T.Add(Object o) permettant d'ajouter au tableau T un objet o. Ici l'objet o sera un objet personne.
la proprit T.Count qui donne le nombre d'lments du tableau T
la notation T[i] qui donne l'lment i du tableau T
La classe listeDePersonnes va hriter de tous les attributs, mthodes et proprits de la classe ArrayList.
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.
Enfin nous redfinissons l'oprateur + afin de pouvoir crire l'opration l+p o l est un objet listeDePersonnes et p un objet personne.
L'opration l+p ajoute la personne p la liste de personnes l. Cette opration rend une valeur de type listeDePersonnes, ce qui permet
d'enchaner les oprateurs + comme il a t expliqu plus haut. Ainsi l'opration l+p1+p2 est interprte (priorit des oprateurs)
comme (l+p1)+p2. L'opration l+p1 rend une nouvelle liste liste de personnes l1. L'opration (l+p1)+p2 devient alors l1+p2 qui
ajoute la personne p2 la liste de personnes l1.
Le code est le suivant ;
using System;
using System.Collections;
// classe personne
public class listeDePersonnes : ArrayList{
// redfinition oprateur +
// pour ajouter une personne la liste
public static listeDePersonnes operator +(listeDePersonnes l, personne p){
l.Add(p);
return l;
}// operator +
// toString
public override string ToString(){
// rend (l1, l2, ..., ln)
string liste="(";
int i;
// on parcourt le tableau dynamique
for (i=0;i<Count-1;i++){
liste+="["+base[i]+"]"+",";
Classes, Structures, Interfaces
52
}//for
// dernier lmenr
if(Count!=0) liste+="["+base[i]+"]";
liste+=")";
return liste;
}//ToString
public static void Main(){
// une liste de personnes
listeDePersonnes l=new listeDePersonnes();
// ajout de personnes
l=l+new personne("jean",10)+new personne("pauline",12);
// affichage
Console.Out.WriteLine("l="+l);
l=l+new personne ("tintin",27);
Console.Out.WriteLine("l="+l);
}//Main
}//class
Les rsultats :
E:\data\serge\MSNET\c#\objets\8>C:\WINNT\Microsoft.NET\Framework\v1.0.2914\csc.exe /r:personne.dll
lstpersonnes.cs
E:\data\serge\MSNET\c#\objets\8>lstpersonnes
l=([jean,10],[pauline,12])
l=([jean,10],[pauline,12],[tintin,27])
On appelle cette mthode, un indexeur car elle donne un sens 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 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 index ] { virtual get; virtual set; }
Pour indiquer que la mthode public personne this[int i] de la classe listeDePersonnes ne redfinit pas (override) la mthode
public object this[ int index ] 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 get est appele lorsqu'on crit variable=l[i] par exemple o l est une listeDePersonnes. On doit
alors retourner la personne n i de la liste l. Ceci se fait en retournant l'objet n i de la classe ArrayList sous-jacente la classe
listeDePersonnes avec la notation base[i]. L'objet retourn tant de type Object, un transtypage vers la classe personne est ncessaire.
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.
Ici, la personne p reprsente par le mot cl value est affecte l'lment 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; }
}//indexeur
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 :
Classes, Structures, Interfaces
53
// un indexeur
public int this[string N]{
get {
// on recherche la personne de nom N
int i;
for (i=0;i<Count;i++){
if (((personne) base[i]).nom==N) return i;
}//for
return -1;
}//get
}//indexeur[nom]
La premire ligne
public int this[string N]
indique qu'on indexe la classe listeDePersonnes par une chane de caractres N et que le rsultat de l[N] est un entier. Cet entier sera la
position dans la liste de la personne portant le nom N 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 N pass en paramtre. Si on le trouve en position i,
on renvoie i sinon on renvoie -1.
Le code complet de la classe est le suivant :
using System;
using System.Collections;
// classe personne
public class listeDePersonnes : ArrayList{
// redfinition oprateur +
// pour ajouter une personne la liste
public static listeDePersonnes operator +(listeDePersonnes l, personne p){
l.Add(p);
return l;
}// operator +
// un indexeur
public new personne this[int i]{
get { return (personne) base[i]; }
set { base[i]=value; }
}//indexeur
// un indexeur
public int this[string N]{
get {
// on recherche la personne de nom N
int i;
for (i=0;i<Count;i++){
if (((personne) base[i]).nom==N) return i;
}//for
return -1;
}//get
}//indexeur[nom]
// toString
public override string ToString(){
// rend (l1, l2, ..., ln)
string liste="(";
int i;
// on parcourt le tableau dynamique
for (i=0;i<Count-1;i++){
liste+="["+base[i]+"]"+",";
}//for
// dernier lmenr
if(Count!=0) liste+="["+base[i]+"]";
liste+=")";
return liste;
}//ToString
public static void Main(){
// une liste de personnes
listeDePersonnes l=new listeDePersonnes();
// ajout de personnes
l=l+new personne("jean",10)+new personne("pauline",12);
// affichage
Console.Out.WriteLine("l="+l);
l=l+new personne ("tintin",27);
Console.Out.WriteLine("l="+l);
// changement lment 1
l[1]=new personne("milou",5);
// affichage lment 1
Console.Out.WriteLine("l[1]="+l[1]);
// affichage liste l
Classes, Structures, Interfaces
54
Console.Out.WriteLine("l="+l);
// recherche de personnes
string[] noms= new String[] {"milou","haddock"};
for (int i=0;i<noms.Length;i++){
int inom=l[noms[i]];
if(inom!=-1) Console.Out.WriteLine("personne("+noms[i]+")="+l[inom]);
else Console.Out.WriteLine("personne("+noms[i]+") n'existe pas");
}//for
}//Main
}//class
E:\data\serge\MSNET\c#\objets\9>lstpersonnes
l=([jean,10],[pauline,12])
l=([jean,10],[pauline,12],[tintin,27])
l[1]=milou,5
l=([jean,10],[milou,5],[tintin,27])
personne(milou)=milou,5
personne(haddock) n'existe pas
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 :
// structure personne
struct personne{
public string nom;
public int age;
}
// classe PERSONNE
class PERSONNE{
public string nom;
public int age;
}
// une classe de test
public class test
{
static void Main() {
// une personne p1
personne p1;
p1.nom="paul";
p1.age=10;
Console.Out.WriteLine("p1=personne("+p1.nom+","+p1.age+")");
// une personne p2
personne p2=p1;
Console.Out.WriteLine("p2=personne("+p2.nom+","+p2.age+")");
// p2 est modifi
p2.nom="nicole";
p2.age=30;
// vrification p1 et p2
Console.Out.WriteLine("p1=personne("+p1.nom+","+p1.age+")");
Console.Out.WriteLine("p2=personne("+p2.nom+","+p2.age+")");
// une PERSONNE P1
PERSONNE P1=new PERSONNE();
P1.nom="paul";
P1.age=10;
Classes, Structures, Interfaces
55
Console.Out.WriteLine("P1=PERSONNE("+P1.nom+","+P1.age+")");
// une PERSONNE P2
PERSONNE P2=P1;
Console.Out.WriteLine("P2=PERSONNE("+P2.nom+","+P2.age+")");
// P2 est modifi
P2.nom="nicole";
P2.age=30;
// vrification P1 et P2
Console.Out.WriteLine("P1=PERSONNE("+P1.nom+","+P1.age+")");
Console.Out.WriteLine("P2=PERSONNE("+P2.nom+","+P2.age+")");
}//Main
}//classe
L o dans les pages prcdentes de ce chapitre on utilisait une classe personne, nous utilisons maintenant une structure personne :
struct personne{
public string nom;
public int age;
}
La dclaration personne p1; cre une structure (nom,age) et la valeur de p1 est cette structure elle-mme. Rappelons que dans le
cas d'une classe l'opration quivalente tait :
personne p1 = new personne(...);
qui crait un objet personne (grosso modo l'quivalent de notre structure) et p1 tait alors l'adresse (la rfrence) de cet objet.
Rsumons
" dans le cas de la structure, la valeur de p1 est la structure elle-mme
" dans le cas de la classe, la valeur de p1 est l'adresse de l'objet cr
Structure p1
p1
nom
age
Objet p1
nom
age
p1
paul
10
p2
paul
10
La structure de p1 est donc duplique dans p2. C'est une recopie de valeur. Dans le cas des classes, la valeur de p1 est recopie
dans p2, mais comme cette valeur est en fait l'adresse de l'objet, celui-ci n'est pas dupliqu. Il a simplement deux rfrences sur lui :
Classes, Structures, Interfaces
56
p1
--------->
p2
--------->
paul
10
Dans le cas de la structure, si on modifie la valeur de p2 on ne modifie pas la valeur de p1, ce que montre le programme. Dans le cas
de l'objet, si on modifie l'objet point par p2, celui point par p1 est modifi puisque c'est le mme. C'est ce que montrent galement
les rsultats du programme.
On retiendra donc de ces explications que :
"
"
Une fois cette diffrence fondamentale comprise, la structure se montre trs proche de la classe comme le montre le nouvel
exemple suivant :
using System;
// structure personne
struct personne{
// attributs
private string _nom;
private int _age;
// proprits
public string nom{
get {return _nom;}
set {_nom=value;}
}//nom
public int age{
get {return _age;}
set {_age=value;}
}//nom
// Constructeur
public personne(string NOM, int AGE){
_nom=NOM;
_age=AGE;
}//constructeur
// TOSTRING
public override string ToString(){
return "personne("+nom+","+age+")";
}//ToString
}
// une classe de test
public class test {
static void Main() {
// une personne p1
personne p1=new personne("paul",10);
Console.Out.WriteLine("p1="+p1);
// une personne p2
personne p2=p1;
Console.Out.WriteLine("p2="+p2);
// p2 est modifi
p2.nom="nicole";
p2.age=30;
// vrification p1 et p2
Console.Out.WriteLine("p1="+p1);
Console.Out.WriteLine("p2="+p2);
}//Main
}//classe
La seule notable diffrence ici entre structure et classe, c'est qu'avec une classe les objets p1 et p2 auraient eu la mme valeur la fin
du programme, celle de p2.
57
La classe notes rassemble les notes d'une classe dans une matire :
public class notes{
// attribut
protected string matire;
protected lve[] lves;
58
Les attributs sont dclars protected pour tre accessibles d'une classe drive. Le type lve est une structure mmorisant le nom de
l'lve et sa note dans la matire :
public struct lve{
public string nom;
public double note;
Nous dcidons de driver cette classe notes dans une classe notesStats qui aurait deux attributs supplmentaires, la moyenne et l'carttype des notes :
public class notesStats : notes, Istats {
// attribut
private double _moyenne;
private double _cartType;
Cela signifie que la classe notesStats doit avoir deux mthodes appeles moyenne et cartType avec la signature indique dans l'interface
Istats. La classe notesStats est la suivante :
public class notesStats : notes, Istats {
// attribut
private double _moyenne;
private double _cartType;
// constructeur
public notesStats (string MATIERE, lve[] ELEVES): base(MATIERE, ELEVES){
// calcul moyenne des notes
double somme=0;
for (int i=0;i<lves.Length;i++){
somme+=lves[i].note;
}
if(lves.Length!=0) _moyenne=somme/lves.Length;
else _moyenne=-1;
// cart-type
double carrs=0;
for (int i=0;i<lves.Length;i++){
carrs+=Math.Pow((lves[i].note-_moyenne),2);
}//for
if(lves.Length!=0) _cartType=Math.Sqrt(carrs/lves.Length);
else _cartType=-1;
}//constructeur
// ToString
public override string ToString(){
return base.ToString()+",moyenne="+_moyenne+",cart-type="+_cartType;
}//ToString
// mthodes de l'interface Istats
public double moyenne(){
// rend la moyenne des notes
return _moyenne;
}//moyenne
public double cartType(){
// rend l'cart-type
return _cartType;
}//cartType
}//classe
La moyenne _moyenne et l'cart-type _ecartType sont calculs ds la construction de l'objet. Aussi les mthodes moyenne et cartType
n'ont-elles qu' rendre la valeur des attributs _moyenne et _ecartType. Les deux mthodes rendent -1 si le tableau des lves est vide.
La classe de test suivante :
// classe de test
public class test{
public static void Main(){
// qqs lves & notes
lve[] ELEVES=new lve[] { new lve("paul",14),new lve("nicole",16), new lve("jacques",18)};
// qu'on enregistre dans un objet notes
notes anglais=new notes("anglais",ELEVES);
// et qu'on affiche
Console.Out.WriteLine(""+anglais);
// idem avec moyenne et cart-type
anglais=new notesStats("anglais",ELEVES);
Console.Out.WriteLine(""+anglais);
}
Classes, Structures, Interfaces
59
La classe notesStats aurait trs bien pu implmenter les mthodes moyenne et cartType pour elle-mme sans indiquer qu'elle
implmentait l'interface Istats. Quel est donc l'intrt des interfaces ? C'est le suivant : une fonction peut admettre pour paramtre
une donne ayant le type d'une interface I. Tout objet d'une classe C implmentant l'interface I pourra alors tre paramtre de cette
fonction. Considrons l'exemple suivant :
using System;
// une interface Iexemple
public interface Iexemple{
int ajouter(int i,int j);
int soustraire(int i,int j);
}
public class classe1: Iexemple{
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
}//classe
public class classe2: Iexemple{
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
}//classe
L'interface Iexemple dfinit deux mthodes ajouter et soustraire. Les classes classe1 et classe2 implmentent cette interface. On
remarquera que ces classes ne font rien d'autre ceci par souci de simplification de l'exemple. Maintenant considrons l'exemple
suivant :
// classe de test
public class test{
// une fonction statique
private static void calculer(int i, int j, Iexemple inter){
Console.Out.WriteLine(inter.ajouter(i,j));
Console.Out.WriteLine(inter.soustraire(i,j));
}//calculer
// la fonction Main
public static void Main(){
// cration de deux objets classe1 et classe2
classe1 c1=new classe1();
classe2 c2=new classe2();
// appels de la fonction statique calculer
calculer(4,3,c1);
calculer(14,13,c2);
}//Main
}//classe test
La fonction statique calculer admet pour paramtre un lment de type Iexemple. Elle pourra donc recevoir pour ce paramtre aussi
bien un objet de type classe1 que de type classe2. C'est ce qui est fait dans la fonction Main avec les rsultats suivants :
17
21
127
201
On voit donc qu'on a l une proprit proche du polymorphisme vu pour les classes. Si donc un ensemble de classes Ci non lies
entre-elles par hritage (donc on ne peut utiliser le polymorphisme de l'hritage) prsentent un ensemble de mthodes de mme
signature, il peut tre intressant de regrouper ces mthodes dans une interface I dont hriteraient toutes les classes concernes.
Des instances de ces classes Ci peuvent alors tre utilises comme paramtres de fonctions admettant un paramtre 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.
Notons enfin que l'hritage d'interfaces peut tre multiple, c.a.d. qu'on peut crire
public class classeDrive:classeDeBase,i1,i2,..,in{
...
Classes, Structures, Interfaces
60
}
o les ij sont des interfaces.
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.Out.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.NET et qu'on doit donner les rfrences des diffrentes dll contenant les classes que l'on doit utiliser cette information peut
s'avrer utile. On rappelle que pour rfrencer les dll ncessaires la compilation d'une classe, on crit :
csc /r:fic1.dll /r:fic2.dll ... prog.cs
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
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
}
Pour l'exemple, crons dans un espace de noms notre classe personne tudie prcdemment. Nous choisirons istia.st comme espace
de noms. La classe personne devient :
// espaces de noms imports
using System;
// cration de l'espace de nom istia.st
namespace istia.st {
public class personne{
// attributs
private string prenom;
private string nom;
private int age;
Classes, Structures, Interfaces
61
// mthode
public void initialise(string P, string N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// mthode
public void identifie(){
Console.Out.WriteLine(prenom+","+nom+","+age);
}
}// classe personne
}//espace de noms
nous avons import l'espace de noms istia.st avec une clause using :
using istia.st;
Cela produit un fichier test1.exe qui excut donne les rsultats suivants :
Jean,Dupont,30
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 demie 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 :
62
12620.0
13190
15640
24740
31810
39970
48360
55790
92970
127860
151250
172040
195000
0
0
0.05
0.1
0.15
0.2
0.25
0.3
0.35
0.4
0.45
0.50
0.55
0.60
0.65
0
631
1290.5
2072.5
3309.5
4900
6898.5
9316.5
12106
16754.5
23147.5
30710
39312
49062
Chaque ligne a 3 champs. Pour calculer l'impt I, on recherche la premire ligne o QF<=champ1. Par exemple, si QF=23000 on
trouvera la ligne
24740 0.15 2072.5
L'impt I est alors gal 0.15*R - 2072.5*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.65 49062
ce qui donne l'impt I=0.65*R - 49062*nbParts.
La classe impt sera dfinie comme suit :
// cration d'une classe impt
using System;
public class impt{
// les donnes ncessaires au calcul de l'impt
// proviennent d'une source extrieure
private decimal[] limites, coeffR, coeffN;
// constructeur
public impt(decimal[] LIMITES, decimal[] COEFFR, decimal[] COEFFN){
// on vrifie que les 3 tablaeux ont la mme taille
bool OK=LIMITES.Length==COEFFR.Length && LIMITES.Length==COEFFN.Length;
if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la mme taille("+
LIMITES.Length+","+COEFFR.Length+","+COEFFN.Length+")");
// c'est bon
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//constructeur
// calcul de l'impt
public long 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
limites[limites.Length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// retour rsultat
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//calculer
}//classe
Un objet impt est cr avec les donnes permettant le calcul de l'impt d'un contribuable. C'est la partie stable de l'objet. Une fois
cet objet cr, on peut appeler de faon rpte sa mthode calculer qui calcule l'impt du contribuable partir de son statut
marital (mari ou non), son nombre d'enfants et son salaire annuel.
Un programme de test pourait tre le suivant :
using System;
class test
{
public static void Main()
{
// programme interactif de calcul d'impt
// l'utilisateur tape trois donnes au clavier : mari nbEnfants salaire
// le programme affiche alors l'impt payer
Classes, Structures, Interfaces
63
64
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :o 2 d
syntaxe : mari nbEnfants salaire
mari : o pour mari, n pour non mari
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument salaire incorrect : tapez un entier positif ou nul
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :q s d f
syntaxe : mari nbEnfants salaire
mari : o pour mari, n pour non mari
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :o 2 200000
impt=22504 F
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :
65
On tape en (1) le nom de la classe dsire. Cela ramne en (2) divers thmes possibles. On choisit celui qui convient et on a le
rsultat en (3), ici la classe HashTable. Cette mthode convient si on connat le nom de la classe que l'on cherche. Si on veut explorer
la liste des possibilts offertes par la plate-forme .NET ou pourra utiliser le fichier HTML C:\Program
Files\Microsoft.Net\FrameworkSDK\StartHere.htm qui sert de point de dpart pour l'aide sur le SDK :
66
Le lien .NET Framework SDK Documentation estcelui qu'il faut suivre pour avoir une vue d'ensemble des classes .NET :
L, on suivra le lien .NET Framework Class Library. On y trouve la liste de toutes les classes de .NET :
67
Suivons par exemple le lien System.Collections. Cet espace de noms regroupe diverses classes implmentant des collections dont la
classe HashTable :
On y trouve le prototype de la classe ainsi que des exemples d'utilisation. En suivant ci-dessus le lien HashTable Members, on a accs
la description complte de la classe :
68
Cette mthode est la meilleure pour dcouvrir le SDK et ses classes. L'outil WinCV s'avre utile lorsqu'on connat dj un peu la
classe et qu'on a oubli certains de ses membres. WinCV permet de retrouver rapidement la classe et ses membres.
3.2.1 Help/Contents
Prenez l'option Help/Contents du menu.
69
On pourra prendre (no filter) ce qui nous donne accs toute l'aide. Deux aides sont utiless :
l'aide sur le langage C# lui-mme (syntaxe)
l'aide sur les classes.NET utilisables par le langage C#
L'aide au langage C# est accessible via Visual Studio.NET/Visual Basic and Visual C#/Visual C# language/C# Programmer's Reference :
A partir de l, les diffrentes sous-rubriques nous permettent d'avoir de l'aide sur diffrents thmes de C#. On prtera attention aux
tutoriels de C# :
70
Pour avoir accs aux diffrentes classes de la plate-forme .NET, on choisira l'aide Visual Studio.NET/.NET Framework/.NET
Framework Reference/.NET Framework Class Library. On peut alors chercher la classe System.Windows.Forms.Form :
71
3.2.2 Help/Index
L'option Help/index permet de chercher une aide plus cible que l'aide prcdente. Il suffit de taper le mot cl cherch :
72
73
using System;
public class string1{
// une classe de dmonstration
public static void Main(){
string uneChaine="l'oiseau vole au-dessus des nuages";
affiche("uneChaine="+uneChaine);
affiche("uneChaine.Length="+uneChaine.Length);
affiche("chaine[10]="+uneChaine[10]);
affiche("uneChaine.IndexOf(\"vole\")="+uneChaine.IndexOf("vole"));
affiche("uneChaine.IndexOf(\"x\")="+uneChaine.IndexOf("x"));
affiche("uneChaine.LastIndexOf('a')="+uneChaine.LastIndexOf('a'));
affiche("uneChaine.LastIndexOf('x')="+uneChaine.LastIndexOf('x'));
affiche("uneChaine.Substring(4,7)="+uneChaine.Substring(4,7));
affiche("uneChaine.ToUpper()="+uneChaine.ToUpper());
affiche("uneChaine.ToLower()="+uneChaine.ToLower());
affiche("uneChaine.Replace('a','A')="+uneChaine.Replace('a','A'));
string[] champs=uneChaine.Split(null);
for (int i=0;i<champs.Length;i++){
affiche("champs["+i+"]=["+champs[i]+"]");
}//for
affiche("Join(\":\",champs)="+System.String.Join(":",champs));
affiche("(\" abc \").Trim()=["+" abc ".Trim()+"]");
}//Main
public static void affiche(String msg){
// affiche msg
Console.Out.WriteLine(msg);
}//affiche
}//classe
74
La mthode Split de la classe String permet de mettre dans un tableau les champs d'une chane de caractres. La dfinition de la
mthode 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 est un champ de la chane.
value
separator
rsultat
// mthode de classe - rend la position de value dans le tableau tri array - cherche partir de la position index et avec length
lments
public static void Copy(Array sourceArray, Array destinationArray, int length);
// mthode de classe - copie length lments de sourceArray dans destinationArray - destinationArray est cr pour l'occasion
public static void Sort(Array array);
// mthode de classe - trie le tableau array - celui doit contenir un type de donnes ayant une fonction d'ordre par dfaut
(chanes, nombres).
Le programme suivant illustre l'utilisation de la classe Array :
// tableaux
// imports
using System;
public class tab1{
public static void Main(string[] args){
// lecture des lments d'un tableau taps au clavier
Boolean termin=false;
int i=0;
double[] lments1=null;
double[] lments2=null;
double lment=0;
string rponse=null;
Boolean erreur=false;
while (! termin){
// question
Console.Out.Write("Elment (rel) " + i + " du tableau (rien pour terminer) : ");
// lecture de la rponse
rponse=Console.ReadLine().Trim();
// fin de saisie si chane vide
if (rponse.Equals("")) break;
// vrification saisie
try{
lment=Double.Parse(rponse);
erreur=false;
}catch {
Console.Error.WriteLine("Saisie incorrecte, recommencez");
erreur=true;
}//try-catch
// si pas d'erreur
if (! erreur){
// un lmt de plus dans le tableau
i+=1;
// nouveau tableau pour accueillir le nouvel lment
lments2=new double[i];
// copie ancien tableau vers nouveau tableau
if(i!=1) Array.Copy(lments1,lments2,i-1);
// nouveau tableau devient ancien tableau
75
lments1=lments2;
// plus besoin du nouveau tableau
lments2=null;
// insertion nouvel lment
lments1[i-1]=lment;
}//if
}//while
// affichage tableau non tri
System.Console.Out.WriteLine("Tableau non tri");
for (i=0;i<lments1.Length;i++)
Console.Out.WriteLine("lments[" + i + "]=" + lments1[i]);
// tri du tableau
System.Array.Sort(lments1);
// affichage tableau tri
System.Console.Out.WriteLine("Tableau tri");
for (i=0;i<lments1.Length;i++)
Console.Out.WriteLine("lments[" + i + "]=" + lments1[i]);
// Recherche
while ( ! termin){
// question
Console.Out.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.Error.WriteLine("Erreur, recommencez...");
erreur=true;
}//try-catch
// si pas d'erreur
if (! erreur){
// on cherche l'lment dans le tableau tri
i=System.Array.BinarySearch(lments1,0,lments1.Length,lment);
// Affichage rponse
if (i>=0)
Console.Out.WriteLine("Trouv en position " + i);
else Console.Out.WriteLine("Pas dans le tableau");
}//if
}//while
}//Main
}//classe
terminer) : 3.6
terminer)
terminer)
terminer)
terminer)
terminer)
:
:
:
:
:
3,6
7,4
-1,5
-7
La premire partie du programme construit un tableau partir de donnes numriques tapes au clavier. Le tableau ne peut tre
dimensionn priori puisqu'on ne connat pas le nombre d'lments que va taper l'utilisateur. On travaille alors avec deux tableaux
lments1 et lments2.
// un lmt de plus dans le tableau
i+=1;
// nouveau tableau pour accueillir le nouvel lment
lments2=new double[i];
// copie ancien tableau vers nouveau tableau
if(i!=1) Array.Copy(lments1,lments2,i-1);
// nouveau tableau devient ancien tableau
lments1=lments2;
76
Le tableau lments1 contient les valeurs actuellement saisies. Lorsque l'utilisateur ajoute une nouvelle valeur, on construit un tableau
lments2 avec un lment de plus que lments1, on copie le contenu de lments1 dans lments2 (Array.Copy), on fait "pointer"
lments1 sur lments2 et enfin on ajoute le nouvel lment lments1. C'est un processus complexe qui peut tre simplifi si au lieu
d'utiliser un tableau de taille fixe (Array) on utilise un tableau de taille variable (ArrayList).
Le tableau est tri avec la mthode Array.Sort. Cette mthode peut tre appele avec diffrents paramtres prcisant les rgles de
tri. Sans paramtres, c'est ici un tri en ordre croissant qui est fait par dfault. Enfin, la mthode Array.BinarySearch permet de
chercher un lment dans un tableau tri.
efface le tableau
public virtual int IndexOf(object value);
rend la position de l'objet value dans le tableau ou -1 s'il ne s'y trouve pas. Le tableau doit tre tri
public virtual void Sort()
trie le tableau. Celui-ci doit contenir des objets ayant une relation d'ordre prdfinie (chanes, nombres)
public virtual void Sort(System.Collections.IComparer comparer)
77
// question
Console.Out.Write("Elment (rel) " + i + " du tableau (rien pour terminer) : ");
// lecture de la rponse
rponse=Console.ReadLine().Trim();
// fin de saisie si chane vide
if (rponse.Equals("")) break;
// vrification saisie
try{
lment=double.Parse(rponse);
erreur=false;
}catch {
Console.Error.WriteLine("Saisie incorrecte, recommencez");
erreur=true;
}//try-catch
// si pas d'erreur
if (! erreur){
// un lmt de plus dans le tableau
lments.Add(lment);
}//if
}//while
// affichage tableau non tri
System.Console.Out.WriteLine("Tableau non tri");
for (i=0;i<lments.Count;i++)
Console.Out.WriteLine("lments[" + i + "]=" + lments[i]);
// tri du tableau
lments.Sort();
// affichage tableau tri
System.Console.Out.WriteLine("Tableau tri");
for (i=0;i<lments.Count;i++)
Console.Out.WriteLine("lments[" + i + "]=" + lments[i]);
// Recherche
while ( ! termin){
// question
Console.Out.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.Error.WriteLine("Erreur, recommencez...");
erreur=true;
}//try-catch
// si pas d'erreur
if (! erreur){
// on cherche l'lment dans le tableau tri
i=lments.BinarySearch(lment);
// Affichage rponse
if (i>=0)
Console.Out.WriteLine("Trouv en position " + i);
else Console.Out.WriteLine("Pas dans le tableau");
}//if
}//while
}//Main
}//classe
terminer)
terminer)
terminer)
terminer)
terminer)
:
:
:
:
:
6,7
-4,5
8,3
2
78
valeur
valeur1
valeur2
...
Les cls sont uniques, c.a.d. qu'il ne peut y avoir deux cls indentiques. Les mthodes et proprits principales de la classe
Hashtable sont les suivantes :
public Hashtable();
public virtual void Add(object key, object
value);
public virtual
public virtual
public virtual
public virtual
value);
void
void
bool
bool
Remove(object key);
Clear();
ContainsKey(object key);
ContainsValue(object
79
if (dico.ContainsKey(nomCherch)){
value=dico[nomCherch];
Console.Out.WriteLine(nomCherch+","+(String)value);
}else{
Console.Out.WriteLine("Nom " + nomCherch + " inconnu");
}
// recherche suivante
Console.Out.Write("Nom recherch (rien pour arrter) : ");
nomCherch=Console.ReadLine().Trim();
}//while
}//Main
}//Classe
Le programme utilise galement un objet Ienumerator pour parcourir les collections de cls et de valeurs du dictionnaire de type
ICollection (cf ci-dessus les proprits Keys et Values). Une collection est un ensemble d'objets qu'on peut parcourir. L'interface
ICollection est dfinie comme suit :
// from module 'c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll'
public interface System.Collections.ICollection :System.Collections.IEnumerable
{
// Properties
int Count { get; }
bool IsSynchronized { get; }
object SyncRoot { get; }
// Methods
void CopyTo(Array array, int index);
} // end of System.Collections.ICollection
La proprit Count nous permet de connatre le nombre d'lments de la collection. L'interface ICollection drive de l'interface
IEnumerable :
// from module 'c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll'
public interface System.Collections.IEnumerable
{
// Methods
System.Collections.IEnumerator GetEnumerator();
} // end of System.Collections.IEnumerable
Cette interface n'a qu'une mthode GetEnumerator qui nous permet d'obtenir un objet de type IEnumerator :
// from module 'c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll'
public interface System.Collections.IEnumerator
{
// Properties
object Current { get; }
// Methods
bool MoveNext();
80
void Reset();
} // end of System.Collections.IEnumerator
La mthode GetEnumerator() d'une collection ICollection nous permet de parcourir la collection avec les mthodes suivantes :
MoveNext
Current
Reset
positionne sur l'lment suivant de la collection. Rend vrai (true) si cet lment existe, faux (false)
sinon. Le premier MoveNext positionne sur le 1er lment. L'lment "courant" de la collection est
alors disponible dans la proprit Current de l'numrateur
proprit : lment courant de la collection
repositionne l'numrateur en dbut de collection, c.a.d. avant le 1er lment.
La structure d'itration sur les lments d'une collection (ICollection) C est donc la suivante :
// dfinir la collection
ICollection C=...;
// obtenir un numrateur de cette collection
IEnumerator itrateur=C.GetEnumerator();
// parcourir la collection avec cet numrateur
while(itrateur.MoveNext()){
// on a un lment courant
// exploiter itrateur.Current
}//while
ouvre un flux partir du fichier path. Une exception est lance si celui-ci n'existe pas
// mthodes
public virtual void Close()
ferme le flux
public virtual string ReadLine()
81
24740:0,15:2072,5
31810:0,2:3309,5
39970:0,25:4900
48360:0,3:6898,5
55790:0,35:9316,5
92970:0,4:12106
127860:0,45:16754,5
151250:0,5:23147,5
172040:0,55:30710
195000:0,6:39312
0:0,65:49062
E:\data\serge\MSNET\c#\bases\4>file1
12620:0:0
13190:0,05:631
15640:0,1:1290,5
24740:0,15:2072,5
31810:0,2:3309,5
39970:0,25:4900
48360:0,3:6898,5
55790:0,35:9316,5
92970:0,4:12106
127860:0,45:16754,5
151250:0,5:23147,5
172040:0,55:30710
195000:0,6:39312
0:0,65:49062
ouvre un flux d'criture dans le fichier path. Une exception est lance si celui-ci ne peut tre cr.
// proprits
public bool AutoFlush { virtual get; virtual set; }
// si gal vrai, l'criture dans le flux ne passe pas par l'intermdiaire d'une mmoire tampon sinon
l'criture dans le flux n'est pas immdiate : il y a d'abord criture dans une mmoire tampon puis dans le
flux lorsque la mmoire tampon est pleine. Par dfaut c'est le mode bufferis qui est utilis. Il convient bien
pour les flux fichier mais gnralement pas pour les flux rseau.
public string NewLine { virtual get; virtual set; }
pour fixer ou connatre la marque de fin de ligne utiliser par la mthode WriteLine
// mthodes
public virtual void Close()
ferme le flux
public virtual string WriteLine(string value)
82
83
Caractre
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.
(modle) 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 "\)".
x|y
Correspond soit x soit y. Par exemple, "z|foot" correspond "z" ou "foot". "(z|f)oo" correspond
"zoo" ou "foo".
{n}
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,}
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*".
{n,m}
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?".
[xyz]
Jeu de caractres. Correspond l'un des caractres indiqus. Par exemple, "[abc]" correspond "a" dans
"plat".
[^xyz]
Jeu de caractres ngatif. Correspond tout caractre non indiqu. Par exemple, "[^abc]" correspond
"p" dans "plat".
[a-z]
Plage de caractres. Correspond tout caractre dans la srie spcifie. Par exemple, "[a-z]" correspond
tout caractre alphabtique minuscule compris entre "a" et "z".
[^m-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".
\b
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".
\B
Correspond une limite ne reprsentant pas un mot. "en*t\B" correspond "ent" dans "bien entendu".
\d
Correspond un caractre reprsentant un chiffre. quivaut [0-9].
\D
Correspond un caractre ne reprsentant pas un chiffre. quivaut [^0-9].
\f
Correspond un caractre de saut de page.
\n
Correspond un caractre de nouvelle ligne.
\r
Correspond un caractre de retour chariot.
\s
Correspond tout espace blanc, y compris l'espace, la tabulation, le saut de page, etc. quivaut
"[ \f\n\r\t\v]".
\S
Correspond tout caractre d'espace non blanc. quivaut "[^ \f\n\r\t\v]".
\t
Correspond un caractre de tabulation.
\v
Correspond un caractre de tabulation verticale.
\w
Correspond tout caractre reprsentant un mot et incluant un trait de soulignement. quivaut "[AZa-z0-9_]".
\W
Correspond tout caractre ne reprsentant pas un mot. quivaut "[^A-Za-z0-9_]".
\num
Correspond num, o num est un entier positif. Fait rfrence aux correspondances mmorises. Par
exemple, "(.)\1" correspond deux caractres identiques conscutifs.
\n
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.
\xn
Correspond n, o n est une valeur d'chappement hexadcimale. Les valeurs d'chappement
Exemples de classes .NET
84
hexadcimales doivent comprendre deux chiffres obligatoirement. Par exemple, "\x41" correspond
"A". "\x041" quivaut "\x04" & "1". Permet d'utiliser les codes ASCII dans des expressions
rgulires.
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
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.
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
modle
!$
\.$
^//
^\s*\w+\s*$
^\s*\w+\s*\w+\s*$
\bsecret\b
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).
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)
85
Voici un exemple :
// expression rgulires
using System;
using System.Text.RegularExpressions;
public class regexp1{
public static void Main(){
// 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)){
affiche("["+exemplaire1 + "] correspond au
}else{
affiche("["+exemplaire1 + "] ne correspond
}//if
string exemplaire2=" 123a ";
if (regex1.IsMatch(exemplaire2)){
affiche("["+exemplaire2 + "] correspond au
}else{
affiche("["+exemplaire2 + "] ne correspond
}//if
}//Main
modle ["+modle1+"]");
pas au modle ["+modle1+"]");
modle ["+modle1+"]");
pas au modle ["+modle1+"]");
}//classe
et les rsultats d'excution :
[
[
rend une collection d'lments de la chane input correspondant au modle comme le montre l'exemple suivant :
// expression rgulires
using System;
using System.Text.RegularExpressions;
public class regexp1{
public static void Main(){
// 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);
affiche("Modle=["+modle2+"],exemplaire=["+exemplaire3+"]");
affiche("Il y a " + rsultats.Count + " occurrences du modle dans l'exemplaire ");
for (int i=0;i<rsultats.Count;i++){
affiche(rsultats[i].Value+" en position " + rsultats[i].Index);
}//for
}//Main
public static void affiche(string msg){
Console.Out.WriteLine(msg);
}//affiche
}//classe
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. Dans notre exemple, rsultats est l'ensemble des
lments de la chane exemplaire3 correspondant au modle modle2 et rsultats[i] l'un de ces lments. La classe Match a deux
proprits qui nous intressent ici :
" Value : la valeur de l'objet Match donc l'lment correspondant au modle
" Index : la position o l'lment a t trouv dans la chane explore
Exemples de classes .NET
86
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 : indique s'il y a eu correspondance
" Groups : 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
Exemples de classes .NET
87
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 : la valeur de l'objet Group qui l'lment correspondant au contenu d'une paraenthse
" Index : la position o l'lment a t trouv dans la chane explore
88
Le sparateur de champs de lachane est ici un des caractres du tableau separator. 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 que vous ayez 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);
89
90
// ligne suivante
continue;
}//if
// on crit les donnes dans le fichier binaire
output.Write(champs[0]);
output.Write(int.Parse(champs[1]));
// ligne suivante
}//while
// fermeture des fichiers
input.Close();
output.Close();
}//main
}//classe
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
"
l'opration d'criture
// on crit les donnes dans le fichier binaire
output.Write(champs[0]);
output.Write(int.Parse(champs[1]));
La classe BinaryWriter dispose de diffrentes mthodes Write surcharges pour crire les diffrents types de donnes simples
"
Les rsultats de l'excution prcdente vont nous tre donns par le programme qui suit. Celui-ci accepte galement deux
arguments :
// syntaxe pg bin texte
// 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
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 :
// BinaryReader
using System;
using System.IO;
//
//
//
//
91
output=new StreamWriter(arguments[1]);
}catch(Exception){
Console.Error.WriteLine("Impossible d'ouvrir le fichier ["+arguments[1]+"] en criture");
Environment.Exit(3);
}//try-catch
// lecture fichier binaire - criture fichier texte
string nom; // nom d'une personne
int age;
// son ge
// boucle d'exploitation du fichier binaire
while(true){
// lecture nom
try{
nom=input.ReadString();
}catch(Exception){
// fin du fichier
break;
}//try-catch
// lecture age
try{
age=input.ReadInt32();
}catch(Exception){
Console.Error.WriteLine("Le fichier " + arguments[0] +
" ne semble pas avoir un format correct");
break;
}//try-catch
// criture dans fichier texte
output.WriteLine(nom+":"+age);
// personne suivante
}// while
// on ferme tout
input.Close();
output.Close();
}//Main
}//classe
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
"
l'opration de lecture
nom=input.ReadString();
age=input.ReadInt32();
La classe BinaryReader dispose de diffrentes mthodes ReadXX pour lire les diffrents types de donnes simples
"
Si on excute les deux programmes la chane transformant personnes.txt en personnes.bin puis personnes.bin en personnes.txt2 on a :
E:\data\serge\MSNET\c#\fichiers\BINARY~1\1>more personnes.txt
paul : 10
helene : 15
jacques : 11
sylvain : 12
E:\data\serge\MSNET\c#\fichiers\BINARY~1\2>more personnes.txt2
paul:10
helene:15
jacques:11
sylvain:12
E:\data\serge\MSNET\c#\fichiers\BINARY~1\2>dir
29/04/2002 18:19
54 personnes.txt
29/04/2002 18:19
44 personnes.bin
29/04/2002 18:20
44 personnes.txt2
92
La classe de base Form dfinit une fentre de base avec des boutons de fermeture, agrandissement/rduction, une taille ajustable, ...
et gre les vnements sur ces objets graphiques. Ici nous spcialisons la classe de base en lui fixant un titre et ses largeur (300) et
hauteur (100). Ceci est fait dans son constructeur :
public Form1() {
// titre de la fentre
this.Text = "Mon premier formulaire";
// dimensions de la fentre
this.Size=new System.Drawing.Size(300,100);
}//constructeur
Le titre de la fentre est fixe par la proprit Text et les dimensions par la proprit Size. Size est dfini dans l'espace de noms
System.Drawing et est une structure.
La fonction Main lance l'application graphique de la faon suivante :
Application.Run(new Form1());
Interfaces graphiques
93
Un formulaire de type Form1 est cr et affich, puis l'application se met l'coute des vnements qui se produisent sur le
formulaire (clics, dplacements de souris, ...) et fait excuter ceux que le formulaire gre. Ici, notre formulaire ne gre pas d'autre
vnement que ceux grs par la classe de base Form (clics sur boutons fermeture, agrandissement/rduction, changement de taille
de la fentre, dplacement de la fentre, ...).
La proprit Location fixe les coordonnes (110,20) du point suprieur gauche du bouton l'aide d'une structure Point. Les largeur et
hauteur du bouton sont fixes (80,30) l'aide d'une structure Size. La proprit Text du bouton permet de fixer le libell du
bouton. La classe bouton possde un vnement Click dfini comme suit :
public event EventHandler Click;
Un objet EventHandler est construit avec le nom d'une mthode f ayant la signature suivante :
public delegate void EventHandler(
object sender,
EventArgs e
);
Interfaces graphiques
94
Ici, lors d'un clic sur le bouton cmdTest, la mthode cmdTest_Click sera appele. Celle-ci est dfinie comme suit conformment au
modle EventHandler prcdent :
// gestionnaire d'vnement
private void cmdTest_Click(object sender, EventArgs evt){
// il y a eu un clic sur le bouton - on le dit
MessageBox.Show("Clic sur bouton", "Clic sur bouton", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}//cmdTest_Click
La classe MessageBox sert afficher des messages dans une fentre. Nous avons utilis ici le constructeur
public static System.Windows.Forms.DialogResult Show(string text, string caption,
System.Windows.Forms.MessageBoxButtons buttons, System.Windows.Forms.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 :
constante
boutons
AbortRetryIgnore
OK
OKCancel
RetryCancel
YesNo
Interfaces graphiques
95
YesNoCancel
Le paramtre icon peut prendre ses valeurs parmi les constantes suivantes :
Asterisk
Error
Exclamation
idem Warning
Hand
Information
idem Asterisk
None
Question
Stop
idem Stop
idem Hand
Warning
La mthode Show est une mthode statique qui rend un rsultat de type System.Windows.Forms.DialogResult qui est une
numration :
// from module
'c:\winnt\assembly\gac\system.windows.forms\1.0.2411.0__b77a5c561934e089\system.windows.forms.dll'
public enum System.Windows.Forms.DialogResult
Interfaces graphiques
96
Abort = 0x00000003,
Cancel = 0x00000002,
Ignore = 0x00000005,
No = 0x00000007,
None = 0x00000000,
OK = 0x00000001,
Retry = 0x00000004,
Yes = 0x00000006,
} // end of System.Windows.Forms.DialogResult
Pour savoir sur quel bouton a appuy l'utilisateur pour fermer la fentre de type MessageBox on crira :
DialogResult res=MessageBox.Show(..);
if (res==DialogResult.Yes){ // il a appuy sur le bouton oui...}
2.
4
3
5
3.
slectionnez le type de projet que vous voulez construire, ici un projet C# (1)
slectionnez le type d'application que vous voulez construire, ici une application windows (2)
indiquez dans quel dossier vous voulez placer le sous-dossier du projet (3)
indiquez le nom du projet (4). Ce sera galement le nom du dossier qui contiendra les fichiers du projet
le nom de ce dossier est rappel en (5)
Un certain nombre de dossiers et de fichiers sont alors crs sous le dossier projet1 :
De ces fichiers, seul un est intressant, le fichier form1.cs qui est le fichier source associ au formulaire cr par VS.NET. Nous y
reviendrons.
En prenant des contrles dans la barre d'outils (toolbox 2) et en les dposant sur la surface de la fentre (1), nous pouvons
construire une interface graphique. Si nos amenons la souris sur la "toolbox" celle-ci s'agrandit et laisse apparatre un
certain nombre de contrles :
Interfaces graphiques
98
Pour l'instant, nous n'en utilisons aucun. Toujours sur l'cran de VS.NET, nous trouvons la fentre de l'explorateur de
solutions "Explorer Solution" :
Dans un premier temps, nous ne nous servirons peu de cette fentre. Elle montre l'ensemble des fichiers formant le projet.
Un seul d'entre-eux nous intresse : le fichier source de notre programme, ici Form1.cs. En cliquant droit sur Form1.cs, on
obtient un menu permettant d'accder soit au code source de notre interface graphique (View code) soit l'interface
graphique elle-mme (View Designer) :
On peut accder ces deux entits directement partir de la fentre "Solution Explorer" :
Ici Form1.cs[Design] dsigne la fentre de conception et Form1.cs la fentre de code. Il suffit de cliquer sur l'un des onglets
pour passer d'une fentre l'autre.
Une autre fentre importante prsente sur l'cran de VS.NET est la fentre des proprits :
Interfaces graphiques
99
Les proprits exposes dans la fentre sont celles du contrle actuellement slectionn dans la fentre de conception
graphique.
On a accs diffrentes fentres du projet avec le menu View :
On y retrouve les fentres principales qui viennent d'tre dcrites ainsi que leurs raccourcis clavier.
System;
System.Drawing;
System.Collections;
System.ComponentModel;
System.Windows.Forms;
System.Data;
namespace projet1
{
/// <summary>
/// Summary description for Form1.
/// </summary>
public class Form1 : System.Windows.Forms.Form
Interfaces graphiques
100
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.Container components = null;
public Form1()
{
//
// Required for Windows Form Designer support
//
InitializeComponent();
//
// TODO: Add any constructor code after InitializeComponent call
//
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.Size = new System.Drawing.Size(300,300);
this.Text = "Form1";
}
#endregion
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.Run(new Form1());
}
}
La classe de base Form dfinit une fentre de base avec des boutons de fermeture, agrandissement/rduction, une taille ajustable, ...
et gre les vnements sur ces objets graphiques.
Le constructeur du formulaire utilise une mthode InitializeComponent dans laquelle les contrles du formulaires sont crs et
initialiss.
public Form1()
{
InitializeComponent();
// autres initialisations
}
Tout autre travail faire dans le constructeur peut tre fait aprs l'appel InitializeComponent. La mthode InitializeComponent
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.Size = new System.Drawing.Size(300,300);
this.Text = "Form1";
}
fixe le titre de la fentre "Form1", sa largeur (300) et sa hauteur (300). Le titre de la fentre est fixe par la proprit Text et les
dimensions par la proprit Size. Size est dfini dans l'espace de noms System.Drawing et est une structure.
La fonction Main lance l'application graphique de la faon suivante :
Interfaces graphiques
101
Application.Run(new Form1());
Un formulaire de type Form1 est cr et affich, puis l'application se met l'coute des vnements qui se produisent sur le
formulaire (clics, dplacements de souris, ...) et fait excuter ceux que le formulaire gre. Ici, notre formulaire ne gre pas d'autre
vnement que ceux grs par la classe de base Form (clics sur boutons fermeture, agrandissement/rduction, changement de taille
de la fentre, dplacement de la fentre, ...).
Le formulaire utilise un attribut components qui n'est utilis nulle part. La mthode dispose ne sert galement rien ici. Il en de mme
de certains espaces de noms (Collections, ComponentModel, Data) utiliss et de celui dfini pour le projet projet1. Aussi, dans cet
exemple le code peut tre simplifi ce qui suit :
using System;
using System.Drawing;
using System.Windows.Forms;
public class Form1 : System.Windows.Forms.Form
{
// constructeur
public Form1()
{
// construction du formulaire avec ses composants
InitializeComponent();
// autres initialisations
}//constructeur
private void InitializeComponent()
{
// taille de la fentre
this.Size = new System.Drawing.Size(300,300);
// titre de la fentre
this.Text = "Form1";
}
static void Main()
{
// on lance l'appli
Application.Run(new Form1());
}
}
4.2.5 Conclusion
Nous accepterons maintenant tel quel le code gnr par VS.NET et nous contenterons d'y ajouter le ntre notamment pour grer
les vnements lis aux diffrents contrles du formulaire.
Interfaces graphiques
102
1
3
nom
type
lblSaisie
Label
txtSaisie
TextBox
cmdAfficher Button
rle
un libell
une zone de saisie
pour afficher dans une bote de dialogue le contenu de la zone de saisie txtSaisie
103
Text
Name
Une fois le composant choisi dans le "toolbox", utilisez la touche "Echap" pour faire disparatre
la barre d'outils, puis dposez et dimensionnez le composant. faites-le pour les trois composants
ncessaires : Label, TextBox, Button. Pour aligner et dimensionner correctement les composants,
utilisez le menu Format :
Interfaces graphiques
104
%
%
l'tiquette 1 (Label)
Slectionnez le composant pour avoir sa fentre de
proprits. Dans celle-ci, modifiez les proprits suivantes :
name : lblSaisie, text : Saisie
le champ de saisie 2 (TextBox)
Slectionnez le composant pour avoir sa fentre de
proprits. Dans celle-ci, modifiez les proprits suivantes :
name : txtSaisie, text : ne rien mettre
le bouton 3 (Button) : name : cmdAfficher, text : Afficher
la fentre elle-mme : name : frmSaisies&Boutons, text :
Saisies & boutons - 1
Fermez la fentre. Il nous reste crire la procdure lie un clic sur le bouton Afficher. Slectionnez le bouton pour avoir accs sa
fentre de proprits. Celle-ci a plusieurs onglets :
Properties
Events
Les proprits et vnements d'un contrle sont accessibles par catgories ou par ordre alphabtique :
Interfaces graphiques
105
Categorized
Alphabetic
La colonne de gauche de la fentre liste les vnements possibles sur le bouton. Un clic sur un bouton correspond l'vnement
Click. La colonne de droite contient le nom de la procdure appele lorsque l'vnement correspondant se produit. Double-cliquez
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 cmdAfficher :
Nous n'utiliserons aucun de ces paramtres ici. Il ne nous reste plus qu' complter le code. Ici, nous voulons prsenter une bote
de dialogue avec dedans le contenu du champ txtSaisie :
private void cmdAfficher_Click(object sender, System.EventArgs e) {
// on affiche le texte qui a t saisi dans la bote de saisie TxtSaisie
MessageBox.Show("texte saisi= "+txtSaisie.Text,"Vrification de la
saisie",MessageBoxButtons.OK,MessageBoxIcon.Information);
}
Interfaces graphiques
106
sert ajouter un gestionnaire pour l'vnement Click sur le bouton cmdAfficher. Ce gestionnaire doit tre de type System.EventHandler.
Le constructeur de cette classe admet un paramtre qui est la rfrence d'une mthode dont le prototype doit tre void f(object,
EventArgs). Le premier paramtre est la rfrence de l'objet la source de l'vnement, ici le bouton. Le second paramtre est un
objet de type EventArgs ou d'une classe drive.
// from module 'c:\winnt\microsoft.net\framework\v1.0.2914\mscorlib.dll'
public class EventArgs :
object
{
// Fields
public static readonly EventArgs Empty;
// Constructors
public EventArgs();
// Methods
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public Type GetType();
public virtual string ToString();
} // end of System.EventArgs
Le type EventArgs est trs gnral et n'apporte en fait aucune information. Pour un clic sur un bouton, c'est suffisant. Pour un
dplacement de souris sur un formulaire, on aurait un vnement dfini par :
public event MouseEventHandler MouseMove
La classe MouseEventHandler est dfinie comme :
public delegate void MouseEventHandler(
object sender,
MouseEventArgs e
)
C'est une fonction dlgue (delegate) de fonctions de signature void f (object, MouseEventArgs). La classe MouseEventArgs est elle
dfinie par :
// from module
'c:\winnt\assembly\gac\system.windows.forms\1.0.2411.0__b77a5c561934e089\system.windows.forms.dll'
public class System.Windows.Forms.MouseEventArgs :
EventArgs
{
// Fields
// Constructors
Interfaces graphiques
107
On voit que 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.
4.3.2 Conclusion
Des deux projets tudis, nous pouvons conclure qu'une fois l'interface graphique construite avec VS.NET, le travail du
dveloppeur consiste crire les gestionnaires des vnements qu'il veut grer pour cette interface graphique.
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 :
108
MessageBox.Show("Evt Closed","Closed");
1
2
4
3
n
1
2
3
4
type
TextBox
Label
Button
Button
nom
txtSaisie
lblControle
cmdEffacer
cmdQuitter
rle
champ de saisie
affiche le texte de 1 en temps rel
pour effacer les champs 1 et 2
pour quitter l'application
Le code pertinent de cette application est celui des trois gestionnaires d'vnements :
private void cmdQuitter_Click(object sender, System.EventArgs e) {
// clic sur bouton Quitter
// on quitte l'application
Application.Exit();
}
private void txtSaisie_TextChanged(object sender, System.EventArgs e) {
// le contenu du TextBox a chang
// on le copie dans le Label lblControle
lblControle.Text=txtSaisie.Text;
}
private void cmdEffacer_Click(object sender, System.EventArgs e) {
// on efface le contenu de la bote de saisie
Interfaces graphiques
109
txtSaisie.Text="";
On notera la faon de terminer l'application dans la procdure cmdQuitter_Click : Application.Exit(). On se rappellera ici comment elle
est lance dans la procdure Main de la classe :
static void Main()
{
// on affiche le formulaire frmSaisies
Application.Run(new frmSaisies());
}
3
La liste des contrles est la suivante :
n
1
2
3
type
TextBox
TextBox
Button
nom
txtMultiLignes
txtAjout
btnAjouter
rle
champ de saisie multilignes
champ de saisie monoligne
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)
Le code utile est celui qui traite le clic sur le bouton Ajouter :
private void btnAjouter_Click(object sender, System.EventArgs e) {
// ajout du contenu de txtAjout celui de txtMultilignes
txtMultilignes.Text+=txtAjout.Text;
txtAjout.Text="";
}
Interfaces graphiques
110
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 Style :
Simple
DropDown
DropDownList
Par dfaut, le type d'un ComboBox est DropDown. Pour dcouvrir la classe ComboBox, tapez ComboBox dans l'index de l'aide
(Help/Index).
La classe ComboBox a un seul constructeur :
new ComboBox()
C'est une proprit indexe, Items[i] dsignant l'lment i du Combo. Elle est en lecture seule. La classe
ComboBox.ObjectCollection est dfinie comme suit :
// from module
'c:\winnt\assembly\gac\system.windows.forms\1.0.2411.0__b77a5c561934e089\system.windows.forms.dll'
public class System.Windows.Forms.ComboBox+ObjectCollection :
object,
System.Collections.IList,
System.Collections.ICollection,
System.Collections.IEnumerable
{
// Fields
// Constructors
public ObjectCollection(System.Windows.Forms.ComboBox owner);
// Properties
public int Count { virtual get; }
public bool IsReadOnly { virtual get; }
public object this[ int index ] { virtual get; virtual set; }
// Methods
public int Add(object item);
public void AddRange(object[] items);
public virtual void Clear();
public virtual bool Contains(object value);
public void CopyTo(object[] dest, int arrayIndex);
public virtual bool Equals(object obj);
public virtual System.Collections.IEnumerator GetEnumerator();
public virtual int GetHashCode();
public Type GetType();
public virtual int IndexOf(object value);
public virtual void Insert(int index, object item);
public virtual void Remove(object value);
public virtual void RemoveAt(int index);
public virtual string ToString();
} // end of System.Windows.Forms.ComboBox+ObjectCollection
On peut s'tonner qu'un combo puisse contenir des objets alors qu'habituellement il contient des chanes de caractres. Au niveau
visuel, ce sera le cas. Si un ComboBox contient un objet obj, il affiche la chane obj.ToString(). On se rappelle que tout objet une
mthode ToString hrite de la classe object et qui rend une chane de caractres "reprsentative" de l'objet.
L'lment 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.
Interfaces graphiques
111
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.
Nous traitons l'vnement SelectedIndexChanged du combo qui signale un nouvel lment slectionn :
private void cmbNombres_SelectedIndexChanged(object sender, System.EventArgs e) {
// l'lment slectionn chang - on l'affiche
MessageBox.Show("Elment slectionn : (" + cmbNombres.SelectedItem +"," +
cmbNombres.SelectedIndex+")","Combo",MessageBoxButtons.OK, MessageBoxIcon.Information);
}
0
1
2
5
type
Form
nom
Form1
TextBox
txtSaisie
Interfaces graphiques
rle/proprits
formulaire
BorderStyle=FixedSingle
champ de saisie
112
2
3
4
5
6
7
8
Button
ListBox
ListBox
Button
Button
Button
Button
btnAjouter
listBox1
listBox2
btn1TO2
cmd2T0
btnEffacer1
btnEffacer2
Fonctionnement
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. Ce lment est alors transfr dans la bote de saisie pour modification
et enlev de la liste 1.
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 :
private void txtSaisie_TextChanged(object sender, System.EventArgs e) {
// le contenu de txtSaisie a chang
// le bouton Ajouter n'est allum que si la saisie est non vide
btnAjouter.Enabled=txtSaisie.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 :
private void listBox1_SelectedIndexChanged(object sender, System.EventArgs e) {
// un lment a t slectionn
// on allume le bouton de transfert 1 vers 2
btn1TO2.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
btn2TO1.Enabled=true;
}
113
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 :
private void btnEffacer1_Click(object sender, System.EventArgs e) {
// on efface la liste 1
listBox1.Items.Clear();
}
private void btnEffacer2_Click(object sender, System.EventArgs e) {
// on efface la liste 2
listBox2.Items.Clear();
}
qui transfre dans la liste l2 l'lment slectionn dans la liste l1. Cela nous permet d'avoir une seule mthode au lieu de deux pour
transfrer un lment de listBox1 vers listBox2 ou de listBox2 vers listBox1. Avat de faire le transfert, on s'assure qu'il y a bien un
lment slectionn dans la liste l1 :
// un lment slectionn ?
if(l1.SelectedIndex==-1) return;
on opre le transfert de la liste X vers la liste Y et on change l'tat de certains boutons pour reflter le nouvel tat des listes.
Interfaces graphiques
114
1
2
3
type
RadioButton
CheckBox
ListBox
nom
radioButton1
radioButton2
radioButton3
chechBox1
chechBox2
chechBox3
lstValeurs
rle
3 boutons radio
3 cases cocher
une liste
Si on construit les trois boutons radio l'un aprs l'autre, ils font partie par dfaut d'un mme groupe. Aussi lorsque l'un est coch,
les autres ne le sont pas. 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 Check qui vrai
signifie que le contrle est coch. Nous avons ici utilis une seule mthode pour traiter les six vnements CheckChanged, la mthode
affiche. Aussi dans la mthode InitializeComponent trouve-t-on les instructions suivantes :
this.checkBox1.CheckedChanged +=
this.checkBox2.CheckedChanged +=
this.checkBox3.CheckedChanged +=
this.radioButton1.CheckedChanged
this.radioButton2.CheckedChanged
this.radioButton3.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 syntaxe if (sender is CheckBox) permet de vrifier si 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 lstValeurs le nom du composant l'origine de
l'vnement et la valeur de sa proprit Checked. A l'excution, 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
115
n
1
2
3
type
hScrollBar
hScrollBar
TextBox
nom
hScrollBar1
hScrollBar2
txtValeur
NumericUpDown
incrmenteur
%
%
%
%
%
%
%
rle
un variateur horizontal
un variateur horizontal qui suit les variations du variateur 1
affiche la valeur du variateur horizontal
ReadOnly=true pour empcher toute saisie
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 est 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 est 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 fix 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 utile de notre application est le suivant :
Le formulaire est mis en forme lors de sa construction :
public Form1()
{
// cration initiale du formulaire
InitializeComponent();
// on donne au variateur 2 les mmes caractristiques qu'au variateur 1
hScrollBar2.Minimum=hScrollBar1.Value;
hScrollBar2.Minimum=hScrollBar1.Minimum;
hScrollBar2.Maximum=hScrollBar1.Maximum;
hScrollBar2.LargeChange=hScrollBar1.LargeChange;
hScrollBar2.SmallChange=hScrollBar1.SmallChange;
// idem pour l'incrmenteur
incrmenteur.Minimum=hScrollBar1.Value;
incrmenteur.Minimum=hScrollBar1.Minimum;
incrmenteur.Maximum=hScrollBar1.Maximum;
incrmenteur.Increment=hScrollBar1.SmallChange;
// on donne au TextBox la valeur du variateur 1
txtValeur.Text=""+hScrollBar1.Value;
}//constructeur
MouseEnter
MouseLeave
MouseMove
MouseDown
MouseUp
DragDrop
DragEnter
DragLeave
DragOver
Voici un programme permettant de mieux apprhender quels moments se produisent les diffrents vnements souris :
type
Label
nom
lblPosition
2
3
ListBox
Button
lstEvts
btnEffacer
rle
pour afficher la position de la souris dans le formulaire 1, la liste 2
ou le bouton 3
pour afficher les vts souris autres que MouseMove
pour effacer le contenu de 2
117
et dans le constructeur (InitializeComponent), on donne le mme gestionnaire d'vnements Form1_MouseMove aux trois contrles :
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseMove);
this.btnEffacer.MouseMove += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseMove);
this.lstEvts.MouseMove += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseMove);
Il faut savoir ici qu' 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 des contrles :
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, Hand pour la liste 2 et No pour le bouton 3 comme
le montrent les copies d'cran ci-dessous.
Dans le code du constructeur, le code gnr par ces choix est le suivant :
this.lstEvts.Cursor = System.Windows.Forms.Cursors.Hand;
this.btnEffacer.Cursor = System.Windows.Forms.Cursors.No;
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 :
// affiche
private void affiche(String message){
// on affiche le message en haut de la liste des evts
lstEvts.Items.Insert(0,message);
}
private void lstEvts_MouseEnter(object sender, System.EventArgs e) {
affiche ("MouseEnter sur liste");
}
private void lstEvts_MouseLeave(object sender, System.EventArgs e) {
affiche ("MouseLeave sur liste");
}
Interfaces graphiques
118
Pour traiter les clics sur le formulaire, nous traitons les vnements MouseDown et MouseUp :
private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e) {
affiche("MouseDown sur formulaire");
}
private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e) {
affiche("MouseUp sur formulaire");
}
1
Le contrle 1 est un TextBox en lecture seule (ReadOnly=true) et de nom txtStatut. L'arborescence du menu est la suivante :
Les options de menu sont des contrles comme les autres composants visuels et ont des proprits et vnements. Par exemple le
tableau des proprits de l'option de menu A1 :
Interfaces graphiques
119
Les proprits des diffrentes options de menu de notre exemple sont les suivantes :
Name
mnuA
mnuA1
mnuA2
mnuA3
mnuB
mnuB1
mnuSep1
mnuB2
mnuB3
mnuB31
mnuB32
Text
options A
A1
A2
A3
options B
B1
- (sparateur)
B2
B3
B31
B32
On a alors un menu vide qui s'installe sur le formulaire avec des cases vides intitules "Type Here". Il suffit d'y indiquer les
diffrentes options du menu :
Pour insrer un sparateur entre deux options comme ci-dessus entre les options B1 et B2, positionnez-vous l'emplacement du
sparateur dans le menu, cliquez droit et prenez l'option Insert Separator :
Interfaces graphiques
120
Si on lance l'application par F5, on obtient un formulaire avec un menu qui pour l'instant ne fait rien.
Les options de menu sont traites comme des composants : elles ont des proprits et des vnements. Dans la structure du menu,
slectionnez l'option A1 et cliquez droit pour avoir accs aux proprits du contrle :
Vous avez alors accs la fentre des proprits dans laquelle on slectionnera l'onglet vnements. Slectionnez les vnements et
tapez affiche en face de l'vnement Click. Cela signifie que l'on souhaite que le clic sur l'option A1 soit traite par une mthode
appele affiche.
Interfaces graphiques
121
Dans cette mthode, nous nous contenterons d'afficher la proprit Text de l'option de menu la source de l'vnement :
private void affiche(object sender, System.EventArgs e) {
// affiche dans le TextBox le nom du sous-menu choisi
txtStatut.Text=((MenuItem)sender).Text;
}
La source de l'vnement sender est de type object. Les options de menu sont elle de type MenuItem, aussi est-on oblig ici de faire un
transtypage de object vers MenuItem.
Pour toutes les options de menu, on fixe le gestionnaire du clic la mthode affiche. Slectionnez par exemple l'option A2 et ses
vnements. En face de l'vnement Click, on a une liste droulante dans laquelle sont prsentes les mthodes existantes pouvant
traiter cet vnement. Ici on n'a que la mthode affiche qu'on slectionne. On rpte ce processus pour tous les composants menu.
Le code utile de cette application outre celui de la mthode affiche est celui de la construction du menu dans le constructeur du
formulaire (InitializeComponent) :
private void InitializeComponent()
{
this.mainMenu1 = new System.Windows.Forms.MainMenu();
this.mnuA = new System.Windows.Forms.MenuItem();
this.mnuA1 = new System.Windows.Forms.MenuItem();
this.mnuA2 = new System.Windows.Forms.MenuItem();
this.mnuA3 = new System.Windows.Forms.MenuItem();
this.mnuB = new System.Windows.Forms.MenuItem();
this.mnuB1 = new System.Windows.Forms.MenuItem();
this.mnuB2 = new System.Windows.Forms.MenuItem();
this.mnuB3 = new System.Windows.Forms.MenuItem();
this.mnuB31 = new System.Windows.Forms.MenuItem();
this.mnuB32 = new System.Windows.Forms.MenuItem();
this.txtStatut = new System.Windows.Forms.TextBox();
this.mnuSep1 = new System.Windows.Forms.MenuItem();
this.SuspendLayout();
//
// mainMenu1
//
this.mainMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
Interfaces graphiques
122
this.mnuA,
this.mnuB});
//
// mnuA
//
this.mnuA.Index = 0;
this.mnuA.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.mnuA1,
this.mnuA2,
this.mnuA3});
this.mnuA.Text = "Options A";
//
// mnuA1
//
this.mnuA1.Index = 0;
this.mnuA1.Text = "A1";
this.mnuA1.Click += new System.EventHandler(this.affiche);
//
// mnuA2
//
this.mnuA2.Index = 1;
this.mnuA2.Text = "A2";
this.mnuA2.Click += new System.EventHandler(this.affiche);
//
// mnuA3
//
this.mnuA3.Index = 2;
this.mnuA3.Text = "A3";
this.mnuA3.Click += new System.EventHandler(this.affiche);
//
// mnuB
//
this.mnuB.Index = 1;
this.mnuB.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.mnuB1,
this.mnuSep1,
this.mnuB2,
this.mnuB3});
this.mnuB.Text = "Options B";
//
// mnuB1
//
this.mnuB1.Index = 0;
this.mnuB1.Text = "B1";
this.mnuB1.Click += new System.EventHandler(this.affiche);
//
// mnuB2
//
this.mnuB2.Index = 2;
this.mnuB2.Text = "B2";
this.mnuB2.Click += new System.EventHandler(this.affiche);
//
// mnuB3
//
this.mnuB3.Index = 3;
this.mnuB3.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.mnuB31,
this.mnuB32});
this.mnuB3.Text = "B3";
this.mnuB3.Click += new System.EventHandler(this.affiche);
//
// mnuB31
//
this.mnuB31.Index = 0;
this.mnuB31.Text = "B31";
this.mnuB31.Click += new System.EventHandler(this.affiche);
//
// mnuB32
//
this.mnuB32.Index = 1;
this.mnuB32.Text = "B32";
this.mnuB32.Click += new System.EventHandler(this.affiche);
//
// txtStatut
//
this.txtStatut.Location = new System.Drawing.Point(8, 8);
this.txtStatut.Name = "txtStatut";
this.txtStatut.ReadOnly = true;
this.txtStatut.Size = new System.Drawing.Size(112, 20);
this.txtStatut.TabIndex = 0;
this.txtStatut.Text = "";
//
// mnuSep1
//
this.mnuSep1.Index = 1;
this.mnuSep1.Text = "-";
//
// Form1
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(136, 42);
this.Controls.AddRange(new System.Windows.Forms.Control[] {
Interfaces graphiques
123
this.txtStatut});
this.Menu = this.mainMenu1;
this.Name = "Form1";
this.Text = "Menus";
this.ResumeLayout(false);
}
type
TextBox multilignes
Button
Button
Button
nom
txtTexte
btnSauvegarder
btnCharger
btnEffacer
rle
texte tap par l'utilisateur ou charg partir d'un fichier
permet de sauvegarder le texte de 1 dans un fichier texte
permet de charger le contenu d'un fichier texte dans 1
efface le contenu de 1
Lorsqu'ils sont pris dans le "ToolBox " et dposs sur le formulaire, ils sont placs dans une zone part en bas du formulaire. Les
composants "Dialog" sont pris dans le "ToolBox" :
124
les types de fichiers proposs dans la liste droulante des types de fichiers de la bote de dialogue
le n du type de fichier propos par dfaut dans la liste ci-dessus. Commence 0.
le dossier prsent initialement pour la sauvegarde du fichier
le nom du fichier de sauvegarde indiqu par l'utilisateur
mthode qui affiche la bote de dialogue de sauvegarde. Rend un rsultat de type DialogResult.
Interfaces graphiques
125
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
"
"
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 *.*.
"
saveFileDialog1.FilterIndex = 0;
Interfaces graphiques
126
Ici, ce sont les fichiers de type *.txt qui seront prsents tout d'abord l'utilisateur.
"
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 : txtTexte.Text tout en grant les
exceptions qui peuvent se produire.
La classe OpenFileDialog est trs proche de la classe SaveFileDialog et est dfinie comme suit :
// from module
'c:\winnt\assembly\gac\system.windows.forms\1.0.2411.0__b77a5c561934e089\system.windows.forms.dll'
public sealed class System.Windows.Forms.OpenFileDialog :
System.Windows.Forms.FileDialog,
System.ComponentModel.IComponent,
IDisposable
{
// Fields
// Constructors
public OpenFileDialog();
// Properties
public bool AddExtension { get; set; }
public bool CheckFileExists { virtual get; virtual set; }
public bool CheckPathExists { get; set; }
public IContainer Container { get; }
public string DefaultExt { get; set; }
public bool DereferenceLinks { get; set; }
public string FileName { get; set; }
public string[] FileNames { get; }
public string Filter { get; set; }
public int FilterIndex { get; set; }
public string InitialDirectory { get; set; }
public bool Multiselect { get; set; }
public bool ReadOnlyChecked { get; set; }
public bool RestoreDirectory { get; set; }
public bool ShowHelp { get; set; }
public bool ShowReadOnly { get; set; }
public ISite Site { virtual get; virtual set; }
public string Title { get; set; }
public bool ValidateNames { get; set; }
// Events
public event EventHandler Disposed;
public event CancelEventHandler FileOk;
public event EventHandler HelpRequest;
// Methods
public virtual System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType);
public virtual void Dispose();
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public virtual object GetLifetimeService();
public Type GetType();
public virtual object InitializeLifetimeService();
public System.IO.Stream OpenFile();
public virtual void Reset();
public System.Windows.Forms.DialogResult ShowDialog();
public virtual string ToString();
} // end of System.Windows.Forms.OpenFileDialog
les types de fichiers proposs dans la liste droulante des types de fichiers de la bote de dialogue
le n du type de fichier propos par dfaut dans la liste ci-dessus. Commence 0.
le dossier prsent initialement pour la recherche du fichier ouvrir
le nom du fichier ouvrir indiqu par l'utilisateur
mthode qui affiche la bote de dialogue de sauvegarde. Rend un rsultat de type DialogResult.
127
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
"
"
"
"
Ici, ce sont les fichiers de type *.txt qui seront prsents tout d'abord l'utilisateur.
La bote de dialogue est affiche et son rsultat rcupr
Interfaces graphiques
128
if(openFileDialog1.ShowDialog() == DialogResult.OK) {
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 ouvrir 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
Ouvrir 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 la mthode qui permet de lire la totalit d'un fichier :
txtTexte.Text=fichier.ReadToEnd();
"
le contenu du fichier est mis dans le TextBox txtTexte. On gre les exceptions qui peuvent se produire.
N
6
7
type
Button
Button
nom
btnCouleur
btnPolice
rle
pour fixer la couleur des caractres du TextBox
pour fixer la police de caractres du TextBox
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 :
Interfaces graphiques
129
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.
La mthode ShowDialog de la classe FontDialog permet de choisir une police de caractres :
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 les lments pour traiter les clics sur les boutons Couleur et Police :
private void btnCouleur_Click(object sender, System.EventArgs e) {
// choix d'une couleur de texte
if(colorDialog1.ShowDialog()==DialogResult.OK){
// on change la proprit forecolor du TextBox
txtTexte.ForeColor=colorDialog1.Color;
}//if
}
private void btnPolice_Click(object sender, System.EventArgs e) {
// choix d'une police de caractres
if(fontDialog1.ShowDialog()==DialogResult.OK){
// on change la proprit font du TextBox
txtTexte.Font=fontDialog1.Font;
Interfaces graphiques
130
}//if
4.7.3 Timer
Nous nous proposons ici d'crire l'application suivante :
n
Type
1
TextBox
ReadOnly=true
2
Button
3
Timer
Nom
txtChrono
affiche un chronomtre
Rle
btnArretMarche
timer1
Le chronomtre en marche :
Le chronomtre arrt :
Pour changer toutes les secondes le contenu du TextBox txtChrono, 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 :
Une fois ce composant install sur le formulaire (dans la partie des composants non visuels), un objet de type Timer est cr dans le
constructeur du formulaire. La classe System.Windows.Forms.Timer est dfinie comme suit :
// from module
'c:\winnt\assembly\gac\system.windows.forms\1.0.2411.0__b77a5c561934e089\system.windows.forms.dll'
public class System.Windows.Forms.Timer :
System.ComponentModel.Component,
System.ComponentModel.IComponent,
IDisposable
{
// Fields
// Constructors
public Timer();
public Timer(System.ComponentModel.IContainer container);
// Properties
public IContainer Container { get; }
public bool Enabled { virtual get; virtual set; }
Interfaces graphiques
131
// Events
public event EventHandler Disposed;
public event EventHandler Tick;
// Methods
public virtual System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType);
public virtual void Dispose();
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public virtual object GetLifetimeService();
public Type GetType();
public virtual object InitializeLifetimeService();
public void Start();
public void Stop();
public virtual string ToString();
} // end of System.Windows.Forms.Timer
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 suivante :
private void btnArretMarche_Click(object sender, System.EventArgs e) {
// arrt ou marche ?
if(btnArretMarche.Text=="Marche"){
// on note l'heure de dbut
dbut=DateTime.Now;
// on l'affiche
txtChrono.Text="00:00:00";
// on lance le timer
timer1.Enabled=true;
// on change le libell du bouton
btnArretMarche.Text="Arrt";
// fin
return;
}//
if (btnArretMarche.Text=="Arrt"){
// arrt du timer
timer1.Enabled=false;
// on change le libell du bouton
btnArretMarche.Text="Marche";
// fin
return;
}
}
Le libell du bouton Arret/Marche est soit "Arrt" soit "Marche". On est donc oblig de faire un test sur ce libell pour savoir quoi
faire.
" dans le cas de "Marche", on note l'heure de dbut dans une variable qui est une variable globale de l'objet formulaire, le
timer est lanc (Enabled=true) et le libell du bouton passe "Arrt".
" dans le cas de "Arrt", on arrte le timer (Enabled=false) et on passe le libell du bouton "Marche".
public class Form1 : System.Windows.Forms.Form {
private System.Windows.Forms.Timer timer1;
private System.Windows.Forms.Button btnArretMarche;
private System.ComponentModel.IContainer components;
private System.Windows.Forms.TextBox txtChrono;
private System.Windows.Forms.Label label1;
// variables d'instance
private DateTime dbut;
L'attribut dbut ci-dessus est connu dans toutes les mthodes de la classe. Il nous reste traiter l'vnement Tick sur l'objet timer1,
vnement qui se produit toutes les secondes :
private void timer1_Tick(object sender, System.EventArgs e) {
// une seconde s'est coule
DateTime maintenant=DateTime.Now;
TimeSpan dure=maintenant-dbut;
// on met jour le chronomtre
txtChrono.Text=""+dure.Hours.ToString("d2")+":"+dure.Minutes.ToString("d2")+":"+dure.Seconds.ToString
("d2");
}
Interfaces graphiques
132
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. 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.
1
3
4
5
6
7
type
RadioButton
RadioButton
NumericUpDown
nom
rdOui
rdNon
incEnfants
4
5
TextBox
TextBox
txtSalaire
txtImpots
6
7
8
Button
Button
Button
btnCalculer
btnEffacer
btnQuitter
rle
coch si mari
coch si pas mari
nombre d'enfants du contribuable
Minimum=0, Maximum=20, Increment=1
salaire annuel du contribuable en F
montant de l'impt payer
ReadOnly=true
lance le calcul de l'impt
remet le formulaire dans son tat initial lors du chargement
pour quitter l'application
Rgles de fonctionnement
"
"
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 :
Interfaces graphiques
133
Le programme est donn ci-dessous. Il utilise la classe impot cre dans le chapitre sur les classes. Une partie du code produit
automatiquement pas VS.NET n'a pas t ici reproduit.
using
using
using
using
using
using
System;
System.Drawing;
System.Collections;
System.ComponentModel;
System.Windows.Forms;
System.Data;
134
Interfaces graphiques
135
5. Gestion d'vnements
Nous avons dans le chapitre prcdent abord la notion d'vnements lis des composants. Nous voyons maintenant comment
crer des vnements dans nos propres classes.
dfinit un type appel opration qui est en fait 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 fonction fi dans la variable op se fait par op=new opration(fi). Pour ajouter une fonction fj la liste des
fonctions dj enregistres, on crit op+= new opration(fj). Pour enlever une fonction fk dj enregistre on crit op-=new opration(fk).
Si dans notre exemple on crit n=op(n1,n2), l'ensemble des fonctions enregistres dans la variable op seront excutes avec les
paramtres n1 et n2. Le rsultat n rcupr sera celui de la dernire fonction excute. Il n'est pas possible d'obtenir les rsultats
produits par l'ensemble des fonctions. Pour cette raison, si on enregistre une liste de fonctions dans une fonction dlgue, celles-ci
rendent le plus souvent le rsultat void.
Considrons l'exemple suivant :
//fonctions dlgues
using System;
public class class1{
// dfinition d'un prototype de fonction
// accepte 2 entiers en paramtre et rend un entier
public delegate int opration(int n1, int n2);
// deux mthodes d'instance correspondant au prototype
public int ajouter(int n1, int n2){
Console.Out.WriteLine("ajouter("+n1+","+n2+")");
return n1+n2;
}//ajouter
public int soustraire(int n1, int n2){
Console.Out.WriteLine("soustraire("+n1+","+n2+")");
return n1-n2;
}//soustraire
// une mthode statique correspondant au prototype
public static int augmenter(int n1, int n2){
Console.Out.WriteLine("augmenter("+n1+","+n2+")");
return n1+2*n2;
}//augmenter
// programme de test
public static void Main(){
// on dfinit un objet de type opration pour y enregistrer des fonctions
// on enregistre la fonction statique augmenter
opration op=new opration(class1.augmenter);
// on excute le dlgu
int n=op(4,7);
Console.Out.WriteLine("n="+n);
// cration d'un objet c1 de type class1
class1 c1=new class1();
// on enregistre dans le dlgu la mthode ajouter de c1
Interfaces graphiques
136
op=new class1.opration(c1.ajouter);
// excution de l'objet dlgu
n=op(2,3);
Console.Out.WriteLine("n="+n);
// on enregistre dans le dlgu la mthode soustraire de c1
op=new class1.opration(c1.soustraire);
n=op(2,3);
Console.Out.WriteLine("n="+n);
//enregistrement de deux fonctions dans le dlgu
op=new class1.opration(c1.ajouter);
op+=new class1.opration(c1.soustraire);
// excution de l'objet dlgu
op(0,0);
// on retire une fonction du dlgu
op-=new opration(c1.soustraire);
// on excute le dlgu
op(1,1);
}//Main
}//classe
137
}//ToString
// le prototype des fonctions charges de traiter l'vt
public delegate void _evtHandler(object sender, myEventArgs evt);
// le pool des gestionnaires d'vts
public event _evtHandler evtHandler;
// mthode de demande d'mission d'un vt
public void envoyerEvt(myEventArgs evt){
// on prvient tous les abonns
// on fait comme si l'vt provenait d'ici
evtHandler(this,evt);
}//envoyerEvt
}//metteur
// une classe de traitement de l'vt
public class souscripteur{
// attribut
private string nom; // nom du souscripteur
private metteur sender; // l'metteur des vts
// constructeur
public souscripteur(string nom, metteur e){
// on note le nom du souscripteur
this.nom=nom;
// et l'metteur des vts
this.sender=e;
// on s'inscrit pour recevoir les evts de l'metteur e
e.evtHandler+=new metteur._evtHandler(traitementEvt);
}//souscripteur
// gestionnaire d'vt
public void traitementEvt(object sender, myEventArgs evt){
// affichage evt
Console.Out.WriteLine("L'objet [" + sender + "] a signal la saisie errone [" + evt + "] au
souscripteur [" + nom + "]");
}//traitement1
}// classe souscripteur
// un programme de test
public class test{
public static void Main(){
// cration d'un metteur d'evts
metteur metteur1=new metteur("metteur1");
// cration d'un tableau de souscripteurs
// pour les vts mis par metteur1
souscripteur[] souscripteurs=new souscripteur[] {new souscripteur("s1",metteur1), new
souscripteur("s2",metteur1)};
// on lit une suite d'entiers au clavier
// ds que l'un est erron, on met un vt
Console.Out.Write("Nombre entier (rien pour arrter) : ");
string saisie=Console.In.ReadLine().Trim();
// tant que la ligne saisie est non vide
while(saisie!=""){
// la saisie est-elle un nombre entier ?
try{
int n=int.Parse(saisie);
}catch(Exception){
// ce n'est pas un entier
// on prvient tout le monde
metteur1.envoyerEvt(new myEventArgs(saisie));
}//try-catch
// nouvelle saisie
Console.Out.Write("Nombre entier (rien pour arrter) : ");
saisie=Console.In.ReadLine().Trim();
}//while
// fin
Environment.Exit(0);
}//Main
}//classe test
Le code prcdent est assez complexe. Dtaillons-le. Dans une gestion d'vnements, il y a un metteur d'vnements (sender) qui
envoie le dtail des vnements (EventArgs) des souscripteurs qui se sont dclars intresss par les vnements en question. La
classe mettrice des vnements est ici la suivante :
public class metteur{
// la classe mettrice d'un vt
// attribut
private string _nom; // nom de l'metteur
// constructeur
public metteur(string nom){
_nom=nom;
}//constructeur
// ToString
public override string ToString(){
Interfaces graphiques
138
return _nom;
}//ToString
// le prototype des fonctions charges de traiter l'vt
public delegate void _evtHandler(object sender, myEventArgs evt);
// le pool des gestionnaires d'vts
public event _evtHandler evtHandler;
// mthode de demande d'mission d'un vt
public void envoyerEvt(myEventArgs evt){
// on prvient tous les abonns
// on fait comme si l'vt provenait d'ici
evtHandler(this,evt);
}//envoyerEvt
}//metteur
Chaque objet metteur a un nom fix par construction. La mthode ToString a t redfinie de telle faon que lorsqu'on transforme un
objet metteur en string c'est son nom qu'on rcupre. La classe dfinit le prototype des fonctions qui peuvent traiter les vnements
qu'elle met :
public delegate void _evtHandler(object sender, myEventArgs evt);
Le premier argument d'une fonction de traitement d'un vnement sera l'objet qui met l'vnement. Le second argument sera de
type myEventArgs, un objet qui donnera des dtails sur l'vnement et sur lequel nous reviendrons.
Une fois le prototype (delegate) des gestionnaires d'vnements dfini, il nous faut un objet pour enregistrer des fonctions
correspondant ce prototype et qui seront excutes lorsque l'objet qui les enregistre recevra les paramtres permettant de les
excuter.
public event _evtHandler evtHandler;
On notera le mot cl event qui impose au delegate d'tre de type void f(object, EventArgs). Les fonctions enregistres dans evtHandler
seront excutes par une instruction evtHandler(o,evt) o o est de type object ou driv et evt de type myEventArgs ou driv. Cette
excution ne peut se produire que dans un objet de la classe qui dfinit l'vnement evtHandler, c.a.d. un objet de type metteur. Afin
de pouvoir dclencher un vnement de l'extrieur d'un objet metteur, nous ajoutons la classe la mthode envoyerEvt :
// mthode de demande d'mission d'un vt
public void envoyerEvt(myEventArgs evt){
// on prvient tous les abonns
// on fait comme si l'vt provenait d'ici
evtHandler(this,evt);
}//envoyerEvt
Nous devons dfinir quel sera le type d'vnements dclenchs par la classe metteur. Nous avons pour cela dfini la classe
myEventArgs :
public class myEventArgs : EventArgs{
// la classe d'un vt
// attribut
private string _saisie;
// constructeur
public myEventArgs(string saisie){
_saisie=saisie;
}//constructeur
// proprit saisie en lecture seule
public override string ToString(){
return _saisie;
}//ToString
}// classe myEventArgs
Nous avons vu que les fonctions de traitement des vnements devaient correspondre au prototype void f(object, EventArgs).
EventArgs est une classe qui donne des informations sur l'vnement qui s'est produit. Pour chaque nouveau type d'vnement, on
est donc amen driver cette classe. C'est ce qui est fait ici. Les vnements qui vont nous intresser sont des saisies au clavier
errones. On va demander un utilisateur de taper des nombres entiers au clavier et ds qu'il tapera une chane qui ne reprsente
pas un entier, on dclenchera un vnement. Comme dtail de l'vnement, nous nous conterons de donner la saisie errone. C'est
le sens de l'attribut _saisie de la classe. Un objet myEventArgs est donc construit avec pour paramtre la saisie errone. On redfinir
par ailleurs la mthode ToString pour que lorsqu'on transforme un objet myEventArgs en chane, on obtienne l'attribut _saisie.
Nous avons dfini l'metteur des vnements et le type d'vnements qu'il met. Nous dfinissons ensuite le type des souscripteurs
intresss par ces vnements.
// une classe de traitement de l'vt
public class souscripteur{
// attribut
private string nom; // nom du souscripteur
Interfaces graphiques
139
Un souscripteur sera dfini par deux paramtres : son nom (attribut nom) et l'objet metteur dont il veut traiter les vnements
(attribut sender). Ces deux paramtres seront passs au constructeur de l'objet. Au cours de cette mme construction, le souscripteur
s'abonne aux vnements de l'metteur :
e.evtHandler+=new metteur._evtHandler(traitementEvt);
La fonction enregistre auprs de l'metteur est traitementEvt. Cette mthode de la classe souscripteur affiche les deux arguments
qu'elle a reues (sender, evt) ainsi que le nom du rcepteur (nom).
Ont t dfinis, le type des vnements produits, le type de l'metteur de ces vnements, le type des souscripteurs. Il ne nous reste
plus qu' les mettre en oeuvre :
// un programme de test
public class test{
public static void Main(){
// cration d'un metteur d'evts
metteur metteur1=new metteur("metteur1");
// cration d'un tableau de souscripteurs
// pour les vts mis par metteur1
souscripteur[] souscripteurs=new souscripteur[] {new souscripteur("s1",metteur1), new
souscripteur("s2",metteur1)};
// on lit une suite d'entiers au clavier
// ds que l'un est erron, on met un vt
Console.Out.Write("Nombre entier (rien pour arrter) : ");
string saisie=Console.In.ReadLine().Trim();
// tant que la ligne saisie est non vide
while(saisie!=""){
// la saisie est-elle un nombre entier ?
try{
int n=int.Parse(saisie);
}catch(Exception){
// ce n'est pas un entier
// on prvient tout le monde
metteur1.envoyerEvt(new myEventArgs(saisie));
}//try-catch
// nouvelle saisie
Console.Out.Write("Nombre entier (rien pour arrter) : ");
saisie=Console.In.ReadLine().Trim();
}//while
// fin
Environment.Exit(0);
}//Main
}//classe test
Nous crons un tableau de deux souscripteurs pour les vnements mis par l'objet metteur1 :
// cration d'un tableau de souscripteurs
// pour les vts mis par metteur1
souscripteur[] souscripteurs=new souscripteur[] {new souscripteur("s1",metteur1), new
souscripteur("s2",metteur1)};
Nous demandons l'utilisateur de taper des nombres entiers au clavier. Ds qu'une saisie est errone, nous demandons metteur1
d'envoyer un vnement ses souscripteurs :
// on prvient tout le monde
metteur1.envoyerEvt(new myEventArgs(saisie));
Interfaces graphiques
140
L'vnement envoy est de type myEventArgs et contient la saisie errone. Les deux souscripteurs devraient recevoir cet vnement
et le signaler. C'est ce que montre l'excution qui suit.
Les rsultats de l'excution sont les suivants :
E:\data\serge\MSNET\c#\vnements\2>evt1
Nombre entier (rien pour arrter) : 4
Nombre entier (rien pour arrter) : a
L'objet [metteur1] a signal la saisie errone
L'objet [metteur1] a signal la saisie errone
Nombre entier (rien pour arrter) : 1.6
L'objet [metteur1] a signal la saisie errone
L'objet [metteur1] a signal la saisie errone
Nombre entier (rien pour arrter) :
Interfaces graphiques
141
I2
Base de
donnes
Les premires classes permettent un accs direct au SGBD SQL Server de Microsoft sans pilote intermdiaire. Les secondes
permettent l'accs aux sources de donnes OLE DB.
La plate-forme .NET est fournie (mai 2002) avec trois pilotes OLE DB pour respectivement : SQL Server, Oracle et Microsoft Jet
(Access). Si on veut travailler avec une base de donnes ayant un pilote ODBC mais pas de pilote OLE DB, on ne peut pas. Ainsi
on ne peut pas travailler avec le SGBD MySQL qui (mai 2002) ne fournit pas de pilote OLE DB. Il existe cependant une srie de
classes permettant l'accs aux sources de donnes ODBC, les classes odbc.net. Elles ne sont pas livres en standard avec le SDK et
il faut aller les chercher sur le site de Microsoft. Dans les exemples qui vont suivre, nous utiliserons surtout ces classes ODBC car la
plupart des bases de donnes sous windows sont livres avec un tel pilote. Voici par exemple, une liste des pilotes ODBC installs
sur une machine Win 2000 (Menu Dmarrer/Paramtres/Panneau de configuration/Outils d'administration) :
Accs aux bases de donnes
142
143
2.
3.
4.
5.
Dans les deux cas, c'est l'opration d'exploitation et de mise jour des donnes qui prend du temps. Imaginons que ces mises jour
soient faites par un utilisateur faisant des saisies, cette opration peut prendre des dizaines de minutes. Pendant tout ce temps, en
mode connect, la connexion avec la base est maintenue et les modifications immdiatement rpercutes. En mode dconnect, il
n'y a pas de connexion la base pendant la mise jour des donnes. Les modifications sont faites uniquement sur la copie
mmoire. Elles sont rpercutes sur la source de donnes en une seule fois lorsque tout est termin.
Quels sont les avantages et inconvnients des deux mthodes ?
Une connexion est coteuse en ressources systme. S'il y a beaucoup de connexions simultanes, le mode dconnect
permet de rduire leurs dures un minimum. C'est le cas des applications web ayant des milliers d'utilisateurs.
L'inconvnient du mode dconnect est la gestion dlicate des mises jour simultanes. L'utilisateur U1 obtient des
donnes au temps T1 et commence les modifier. Au temps T2, l'utilisateur U2 accde lui aussi la source de donnes et
obtient les mmes donnes. Entre-temps l'utilisateur U1 a modifi certaines donnes mais ne les a pas encore transmises
la source de donnes. U2 travaille donc avec des donnes dont certaines sont errones. Les classes .NET offrent des
solutions pour grer ce problme mais il n'est pas simple rsoudre.
En mode connect, la mise jour simultane de donnes par plusieurs utilisateurs ne pose normalement pas de problme.
La connexion avec la base de donnes tant maintenue, c'est la base de donnes elle-mme qui gre ces mises jour
simultanes. Ainsi Oracle verrouille une ligne de la base de donnes ds qu'un utilisateur la modifie. Elle restera verrouille
donc inaccessible aux autres utilisateurs jusqu' ce que celui qui l'a modifie valide (commit) sa modification ou
l'abandonne (rollback).
Si les donnes doivent circuler sur le rseau, le mode dconnect est choisir. Il permet d'avoir une photo des donnes
dans un objet appel dataset qui reprsente une base de donnes lui tout seul. Cet objet peut circuler sur le rseau entre
machines.
type
code de larticle sur 4 caractres
son nom (chane de caractres)
son prix (rel)
son stock actuel (entier)
le stock minimum (entier) en-dea duquel il faut rapprovisionner larticle
Nous utiliserons cette base aussi bien au travers d'un pilote ODBC qu'un pilote OLE DB afin de montrer la similitude des deux
approches et parce que nous disposons de ces deux types de pilotes pour ACCESS.
Accs aux bases de donnes
144
Nous utiliserons galement une base MySQL DBARTICLES ayant la mme unique table ARTICLES, le mme contenu et accd
au travers d'un pilote ODBC, afin de montrer que l'application crite pour exploiter la base ACCESS n'a pas tre modifie pour
utiliser la base MySQL. La base DBARTICLES est accessible un utilisateur appel admarticles avec le mot de passe mdparticles. La
copie d'cran suivante montre le contenu de la base MySQL :
C:\mysql\bin>mysql --database=dbarticles --user=admarticles --password=mdparticles
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3 to server version: 3.23.49-max-debug
Type 'help' for help.
mysql> show tables;
+----------------------+
| Tables_in_dbarticles |
+----------------------+
| articles
|
+----------------------+
1 row in set (0.01 sec)
mysql> select * from articles;
+------+--------------------------------+------+--------------+---------------+
| code | nom
| prix | stock_actuel | stock_minimum |
+------+--------------------------------+------+--------------+---------------+
| a300 | vlo
| 2500 |
10 |
5 |
| b300 | pompe
|
56 |
62 |
45 |
| c300 | arc
| 3500 |
10 |
20 |
| d300 | flches - lot de 6
| 780 |
12 |
20 |
| e300 | combinaison de plonge
| 2800 |
34 |
7 |
| f300 | bouteilles d'oxygne
| 800 |
10 |
5 |
+------+--------------------------------+------+--------------+---------------+
6 rows in set (0.02 sec)
mysql> describe articles;
+---------------+-------------+------+-----+---------+-------+
| Field
| Type
| Null | Key | Default | Extra |
+---------------+-------------+------+-----+---------+-------+
| code
| text
| YES |
| NULL
|
|
| nom
| text
| YES |
| NULL
|
|
| prix
| double
| YES |
| NULL
|
|
| stock_actuel | smallint(6) | YES |
| NULL
|
|
| stock_minimum | smallint(6) | YES |
| NULL
|
|
+---------------+-------------+------+-----+---------+-------+
5 rows in set (0.00 sec)
mysql> exit
Bye
Pour dfinir la base ACCESS comme source de donnes ODBC, procdez comme suit :
activez l'administrateur de sources de donnes ODBC comme il a t montr plus haut et slectionnez l'onglet User DSN
(DSN=Data Source Name)
145
ajoutez une source avec le bouton Add , indiquez que cette source est accessible via un pilote Access et faites Terminer :
Donnez le nom articles-access la source de donnes, mettez une description libre et utilisez le bouton Slectionner pour
dsigner le fichier .mdb de la base. Terminez par OK.
146
La nouvelle source de donnes apparat alors dans la liste des sources DSN utilisateur :
Pour dfinir la base MySQL DBARTICLES comme source de donnes ODBC, procdez comme suit :
activez l'administrateur de sources de donnes ODBC comme il a t montr plus haut et slectionnez l'onglet User DSN.
Ajoutez une nouvelle source de donnes avec Add et slectionnez le pilote ODBC de MySQL.
1
2
3
4
5
147
Les tapes 2 et 3 sont ralises de faon rpte, la fermeture de connexion nayant lieu qu la fin de lexploitation de la base. Cest
un schma relativement classique dont vous avez peut-tre lhabitude si vous avez exploit une base de donnes de faon
interactive. Ces tapes sont les mmes que la base soit utilise au travers d'un pilote ODBC ou d'un pilote OLE DB. Nous
prsentons ci-dessous un exemple avec les classes .NET de gestion des sources de donnes ODBC. Le programme s'appelle liste et
admet comme paramtre le nom DSN d'une source de donnes ODBC ayant une table ARTICLES. Il affiche alors le contenu de
cette table :
E:\data\serge\MSNET\c#\adonet\5>liste
syntaxe : pg dsnArticles
E:\data\serge\MSNET\c#\adonet\5>liste articles-access
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------a300
b300
c300
d300
e300
f300
vlo
pompe
arc
flches - lot de 6
combinaison de plonge
bouteilles d'oxygne
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
E:\data\serge\MSNET\c#\adonet\5>liste mysql-artices
Erreur d'exploitation de la base de donnes (ERROR [IM002] [Microsoft][ODBC Driver Manager] Data source
name not found and no default driver specified)
E:\data\serge\MSNET\c#\adonet\5>liste mysql-articles
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------a300
b300
c300
d300
e300
f300
vlo
pompe
arc
flches - lot de 6
combinaison de plonge
bouteilles d'oxygne
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
Sur les rsultats ci-dessus, nous voyons que le programme a list aussi bien le contenu de la base ACCESS que de la base MySQL.
Etudions maintenant le code de ce programme :
using System;
using System.Data;
using Microsoft.Data.Odbc;
class listArticles
{
static void Main(string[] args){
// application console
// affiche le contenu d'une table ARRTICLES d'une base DSN
// dont le nom est pass en paramtre
const string syntaxe="syntaxe : pg dsnArticles";
148
Les classes de gestion des sources ODBC se trouvent dans l'espace de noms Microsoft.Data.Odbc qu'on doit donc importer. Par
ailleurs, un certain nombre de classes se trouve dans l'espace de noms System.Data.
using System.Data;
using Microsoft.Data.Odbc;
L'espace de noms Microsoft.Data.Odbc se trouve dans "l'assembly" microsoft.data.odbc.dll. Aussi pour compiler le programme prcdent
crit-on :
csc /r:microsoft.data.odbc.dll liste.cs
uid
password
dsn
data source
...
Si on dfinit une source de donnes comme source de donnes ODBC l'aide de l'administrateur de sources de donnes ODBC,
ces paramtres ont dj t donns et enregistrs. Il suffit alors de passer le paramtre DSN qui donne le nom DSN de la source de
donnes. C'est ce qui est fait ici :
OdbcConnection articlesConn=null; // la connexion
try{
// on tente d'accder la base de donnes
// chane de connexion la base
string connectString="DSN="+dsnArticles+";";
articlesConn = new OdbcConnection(connectString);
articlesConn.Open();
Une fois l'objet OdbcConnection construit, on ouvre la connexion avec la mthode Open. Cette ouverture peut chouer comme toute
autre opration sur la base. C'est pourquoi l'ensemble du code d'accs la base est-il dans un try-catch. Une fois la connexion tablie,
on peut mettre des requtes SQL sur la base.
Dans notre exemple, aprs avoir ouvert la connexion la base, nous mettons une requte SQL SELECT pour avoir le contenu de
la table ARTICLES :
const string tabArticles="articles"; // la table des articles
OdbcDataReader myReader=null;
// le lecteur de donnes
// excution d'une commande SQL
string sqlText = "select * from " + tabArticles;
OdbcCommand myOdbcCommand = new OdbcCommand(sqlText);
myOdbcCommand.Connection = articlesConn;
myReader=myOdbcCommand.ExecuteReader();
150
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.
Le rsultat dun select est une table. Si on considre la table ARTICLES prcdente et quon veuille les noms des articles dont le
stock actuel est au-dessous du seuil minimal, on crira :
select nom from articles where stock_actuel<stock_minimum
Si on les veut par ordre alphabtique des noms, on crira :
select nom from articles where stock_actuel<stock_minimum order by nom
L'exploitation du rsultat d'un select est typiquement une exploitation squentielle analogue celle des fichiers texte : on ne peut
qu'avancer dans la table, pas reculer :
while (objetOdbcDataReadet.Read()){
// on a une ligne - on l'exploite
....
// ligne suivante
}//while
La seule difficult est dans l'instruction o les valeurs des diffrentes colonnes de la ligne courante sont concatnes :
for(i=0;i<myReader.FieldCount;i++){
ligne+=myReader[i]+" ";
}
La notation ligne+=myReader[i] +" " est traduit par ligne+=myReader.Item[i].ToString()+" " o Item[i] est la valeur de la colonne i de la
ligne courante.
151
Ces classes sont dans l'espace de noms System.Data.OleDb. Le programme prcdent peut tre transform aisment pour grer une
base OLE DB :
on remplace partout OdbcXX par OleDbXX
on modifie la chane de connexion. Pour une base ACCESS sans login/mot de passe, la chane de connexion est
Provider=Microsoft.JET.OLEDB.4.0;Data Source=[fichier.mdb]. La partie paramtrable de cette chane est le nom du fichier
ACCESS utiliser. Nous modifierons notre programme pour qu'il accepte en paramtre le nom de ce fichier.
l'espace de noms importer est maintenant System.Data.OleDb.
Notre programme devient le suivant :
using System;
using System.Data;
using System.Data.OleDb;
class listArticles
{
static void Main(string[] args){
// application console
// affiche le contenu d'une table ARRTICLES d'une base DSN
// dont le nom est pass en paramtre
const string syntaxe="syntaxe : pg base_access_articles";
const string tabArticles="articles"; // la table des articles
// vrification des paramtres
// a-t-on 2 paramtres
if(args.Length!=1){
// msg d'erreur
Console.Error.WriteLine(syntaxe);
// fin
Environment.Exit(1);
}//if
// on rcupre le paramtre
string dbArticles=args[0]; // la base de donnes
// prparation de la connexion la bd
OleDbConnection articlesConn=null; // la connexion
OleDbDataReader myReader=null;
// le lecteur de donnes
try{
// on tente d'accder la base de donnes
// chane de connexion la base
string connectString="Provider=Microsoft.JET.OLEDB.4.0;Data Source="+dbArticles+";";
articlesConn = new OleDbConnection(connectString);
articlesConn.Open();
// excution d'une commande SQL
string sqlText = "select * from " + tabArticles;
OleDbCommand myOleDbCommand = new OleDbCommand(sqlText);
myOleDbCommand.Connection = articlesConn;
myReader=myOleDbCommand.ExecuteReader();
// Exploitation de la table rcupre
// affichage des colonnes
string ligne="";
int i;
for(i=0;i<myReader.FieldCount-1;i++){
ligne+=myReader.GetName(i)+",";
152
}//for
ligne+=myReader.GetName(i);
Console.Out.WriteLine("\n"+"".PadLeft(ligne.Length,'-')+"\n"+ligne+"\n"+"".PadLeft(ligne.Length,'')+"\n");
// affichage des donnes
while (myReader.Read()) {
// exploitation ligne courante
ligne="";
for(i=0;i<myReader.FieldCount;i++){
ligne+=myReader[i]+" ";
}
Console.WriteLine(ligne);
}//while
}//try
catch(Exception ex){
Console.Error.WriteLine("Erreur d'exploitation de la base de donnes ("+ex.Message+")");
Environment.Exit(2);
}//catch
finally{
try{
// fermeture lecteur
myReader.Close();
// fermeture connexion
articlesConn.Close();
}catch{}
}//finally
// fin
Environment.Exit(0);
}//main
}//classe
vlo
pompe
arc
flches - lot de 6
combinaison de plonge
bouteilles d'oxygne
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
vlo
pompe
arc
flches - lot de 6
combinaison de plonge
bouteilles d'oxygne
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
Requte SQL (fin pour arrter) : select * from articles where stock_actuel<stock_minimum
153
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------c300 arc
d300 flches - lot de 6
3500 10 20
780 12 20
Requte SQL (fin pour arrter) : insert into articles values ("1","1",1,1,1)
Il y a eu 1 ligne(s) modifie(s)
Requte SQL (fin pour arrter) : select * from articles
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------a300 vlo
b300 pompe
c300 arc
d300 flches - lot de 6
e300 combinaison de plonge
f300 bouteilles d'oxygne
1 1 1 1 1
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
Requte SQL (fin pour arrter) : update articles set nom="2" where nom="1"
Il y a eu 1 ligne(s) modifie(s)
Requte SQL (fin pour arrter) : select * from articles
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------a300 vlo
b300 pompe
c300 arc
d300 flches - lot de 6
e300 combinaison de plonge
f300 bouteilles d'oxygne
1 2 1 1 1
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
Requte SQL (fin pour arrter) : delete from articles where code="1"
Il y a eu 1 ligne(s) modifie(s)
Requte SQL (fin pour arrter) : select * from articles
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------a300
b300
c300
d300
e300
f300
vlo
pompe
arc
flches - lot de 6
combinaison de plonge
bouteilles d'oxygne
2500 10 5
56 62 45
3500 10 20
780 12 20
2800 34 7
800 10 5
Requte SQL (fin pour arrter) : select * from articles order by nom asc
---------------------------------------code,nom,prix,stock_actuel,stock_minimum
---------------------------------------c300
f300
e300
d300
b300
a300
arc
bouteilles d'oxygne
combinaison de plonge
flches - lot de 6
pompe
vlo
3500 10 20
800 10 5
2800 34 7
780 12 20
56 62 45
2500 10 5
System;
System.Data;
Microsoft.Data.Odbc;
System.Text.RegularExpressions;
System.Collections;
154
class sqlCommands
{
static void Main(string[] args){
// application console
// excute des requtes SQL tapes au clavier sur une
// table ARTICLES d'une base DSN dont le nom est pass en paramtre
const string syntaxe="syntaxe : pg dsnArticles";
// vrification des paramtres
// a-t-on 2 paramtres
if(args.Length!=1){
// msg d'erreur
Console.Error.WriteLine(syntaxe);
// fin
Environment.Exit(1);
}//if
// on rcupre le paramtre
string dsnArticles=args[0];
// la base DSN
// chane de connexion la base
string connectString="DSN="+dsnArticles+";";
// prparation de la connexion la bd
OdbcConnection articlesConn=null; // la connexion
OdbcCommand sqlCommand=null;
// la commande SQL
try{
// on tente d'accder la base de donnes
articlesConn = new OdbcConnection(connectString);
articlesConn.Open();
// on cre un objet command
sqlCommand=new OdbcCommand("",articlesConn);
}//try
catch(Exception ex){
// msg d'erreur
Console.Error.WriteLine("Erreur d'exploitation de la base de donnes ("+ex.Message+")");
// libration des ressources
try{articlesConn.Close();} catch {}
// fin
Environment.Exit(2);
}//catch
// on construit un dictionnaire des commandes sql acceptes
string[] commandesSQL=new string[] {"select","insert","update","delete"};
Hashtable dicoCommandes=new Hashtable();
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.Out.Write("\nRequte SQL (fin pour arrter) : ");
requte=Console.In.ReadLine().Trim().ToLower();
// fini ?
if(requte=="fin") break;
// on dcompose la requte en champs
champs=modle.Split(requte);
// requte valide ?
if (champs.Length==0 || ! dicoCommandes.ContainsKey(champs[0])){
// msg d'erreur
Console.Error.WriteLine("Requte invalide. Utilisez select, insert, update, delete");
// requte suivante
continue;
}//if
// prparation de l'objet Command pour excuter la requte
sqlCommand.CommandText=requte;
// excution de la requte
try{
if(champs[0]=="select"){
executeSelect(sqlCommand);
} else executeUpdate(sqlCommand);
}//try
catch(Exception ex){
// msg d'erreur
Console.Error.WriteLine("Erreur d'exploitation de la base de donnes ("+ex.Message+")");
}//catch
// requte suivante
}//while
// libration des ressources
try{articlesConn.Close();} catch {}
// fin
Environment.Exit(0);
}//main
// excution d'une requte de mise jour
static void executeUpdate(OdbcCommand sqlCommand){
155
Nous ne commentons ici que ce qui est nouveau par rapport au programme prcdent :
Nous construisons un dictionnaire des commandes sql acceptes :
// on construit un dictionnaire des commandes sql acceptes
string[] commandesSQL=new string[] {"select","insert","update","delete"};
Hashtable dicoCommandes=new Hashtable();
for(int i=0;i<commandesSQL.Length;i++){
dicoCommandes.Add(commandesSQL[i],true);
}
ce qui nous permet ensuite de vrifier simplement si le 1er mot (champs[0]) de la requte tape est l'une des
quatre commandes acceptes :
// requte valide ?
if (champs.Length==0 || ! dicoCommandes.ContainsKey(champs[0])){
// msg d'erreur
Console.Error.WriteLine("Requte invalide. Utilisez select, insert, update, delete");
// requte suivante
continue;
}//if
Auparavant la requte avait t dcompose en champs l'aide de la mthode Split de la classe RegEx :
Regex modle=new Regex(@"\s+"); // suite d'espaces
// on dcompose la requte en champs
champs=modle.Split(requte);
Les mots composant la requte peuvent tre spars d'un nombre quelconque d'espaces.
L'excution d'une requte select n'utilise pas la mme mthode que celle d'une requte d'une mise jour (insert, update, delete).
Aussi doit-on faire un test et excuter une fonction diffrente pour chacu de ces deux cas :
// excution de la requte
try{
if(champs[0]=="select"){
executeSelect(sqlCommand);
} else executeUpdate(sqlCommand);
}//try
catch(Exception ex){
// msg d'erreur
Console.Error.WriteLine("Erreur d'exploitation de la base de donnes ("+ex.Message+")");
}//catch
L'excution d'une requte SQL peut gnrer une exception qui est ici gre.
Accs aux bases de donnes
156
Nous lui ajoutons un nouveau constructeur permettant d'initialiser les tableaux limites, coeffR, coeffN partir d'une base de donnes
ODBC :
using System.Data;
using Microsoft.Data.Odbc;
using System.Collections;
// constructeur 2
public impt(string DSNimpots, string Timpots, string colLimites, string colCoeffR, string colCoeffN){
// initialise les trois tableaux limites, coeffR, coeffN partir
// du contenu de la table Timpots de la base ODBC DSNimpots
// colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
// peut lancer une exception
string connectString="DSN="+DSNimpots+";"; // chane de connexion la base
OdbcConnection impotsConn=null;
// la connexion
OdbcCommand sqlCommand=null;
// la commande SQL
// la requte SELECT
string selectCommand="select "+colLimites+","+colCoeffR+","+colCoeffN+" from "+Timpots;
// tableaux pour rcuprer les donnes
ArrayList tLimites=new ArrayList();
ArrayList tCoeffR=new ArrayList();
ArrayList tCoeffN=new ArrayList();
// on tente d'accder la base de donnes
impotsConn = new OdbcConnection(connectString);
impotsConn.Open();
// on cre un objet command
sqlCommand=new OdbcCommand(selectCommand,impotsConn);
// on excute la requte
OdbcDataReader myReader=sqlCommand.ExecuteReader();
// Exploitation de la table rcupre
while (myReader.Read()) {
157
Le programme de test est le suivant : il reoit en arguments les paramtres transmettre au constructeur de la classe impt. Aprs
avoir construit un objet impt, il fait des calculs d'impt payer :
using System;
class test
{
public static void Main(string[] arguments)
{
// programme interactif de calcul d'impt
// l'utilisateur tape trois donnes au clavier : mari nbEnfants salaire
// le programme affiche alors l'impt payer
const string syntaxe1="pg DSNimpots tabImpots colLimites colCoeffR colCoeffN";
const string syntaxe2="syntaxe : mari nbEnfants salaire\n"
+"mari : o pour mari, n pour non mari\n"
+"nbEnfants : nombre d'enfants\n"
+"salaire : salaire annuel en F";
// vrification des paramtres du programme
if(arguments.Length!=5){
// msg d'erreur
Console.Error.WriteLine(syntaxe1);
// fin
Environment.Exit(1);
}//if
// on rcupre les arguments
string DSNimpots=arguments[0];
string tabImpots=arguments[1];
string colLimites=arguments[2];
string colCoeffR=arguments[3];
string colCoeffN=arguments[4];
// cration d'un objet impt
impt objImpt=null;
try{
objImpt=new impt(DSNimpots,tabImpots,colLimites,colCoeffR,colCoeffN);
}catch (Exception ex){
Console.Error.WriteLine("L'erreur suivante s'est produite : " + ex.Message);
Environment.Exit(2);
}//try-catch
// boucle infinie
while(true){
// on demande les paramtres du calcul de l'impt
Console.Out.Write("Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour
arrter :");
string paramtres=Console.In.ReadLine().Trim();
// qq chose faire ?
if(paramtres==null || paramtres=="") break;
// vrification du nombre d'arguments dans la ligne saisie
string[] args=paramtres.Split(null);
int nbParamtres=args.Length;
if (nbParamtres!=3){
Console.Error.WriteLine(syntaxe2);
continue;
}//if
// vrification de la validit des paramtres
// mari
string mari=args[0].ToLower();
if (mari!="o" && mari !="n"){
Console.Error.WriteLine(syntaxe2+"\nArgument mari incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants=0;
try{
nbEnfants=int.Parse(args[1]);
if(nbEnfants<0) throw new Exception();
158
}catch (Exception){
Console.Error.WriteLine(syntaxe2+"\nArgument nbEnfants incorrect : tapez un entier positif ou
nul");
continue;
}//if
// salaire
int salaire=0;
try{
salaire=int.Parse(args[2]);
if(salaire<0) throw new Exception();
}catch (Exception){
Console.Error.WriteLine(syntaxe2+"\nArgument salaire incorrect : tapez un entier positif ou
nul");
continue;
}//if
// les paramtres sont corrects - on calcule l'impt
Console.Out.WriteLine("impt="+objImpt.calculer(mari=="o",nbEnfants,salaire)+" F");
// contribuable suivant
}//while
}//Main
}//classe
159
160
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 :
// utilisation de threads
using System;
using System.Threading;
161
ThreadStart est de type delegate et dfinit le prototype d'une fonction sans paramtres. Une construction classique est la
suivante :
Thread T=new Thread(new ThreadStart(run));
2.
3.
4.
5.
162
Le thread principal, celui qui excute la fonction Main, cre 5 autres threads chargs d'excuter la mthode statique affiche. Les
rsultats sont les suivants :
fin du thread main
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
0
1
2
3
4
:
:
:
:
:
: 07:01:03
: 07:01:03
: 07:01:03
: 07:01:03
: 07:01:03
07:01:04
07:01:04
07:01:04
07:01:04
07:01:04
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();
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 le terminer la mthode Main par les instructions :
Les threads d'excution
163
// fin de main
Console.Out.WriteLine("fin du thread " +main.Name);
Environment.Exit(0);
Les threads crs par la fonction Main ne sont pas excuts. C'est l'instruction
Environment.Exit(0);
qui fait cela : elle supprime tous les threads de l'application et non simplement le thread Main. La solution ce problme est que la
mthode Main attende la fin d'excution des threads qu'elle a crs avant de se terminer elle-mme. Cela peut se faire avec la
mthode Join de la classe Thread :
// on attend la fin d'excution de tous les threads
for(int i=0;i<tches.Length;i++){
// attente de la fin d'excution du thread i
tches[i].Join();
}//for
// fin de main
Console.Out.WriteLine("fin du thread " +main.Name + " : " + DateTime.Now.ToString("hh:mm:ss"));
Environment.Exit(0);
0
1
2
3
4
:
:
:
:
:
: 07:14:51
: 07:14:51
: 07:14:51
: 07:14:51
: 07:14:51
07:14:52
07:14:52
07:14:52
07:14:52
07:14:52
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 heures. Il est bien sr hors de question qu'un client monopolise tout seul le
serveur 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.
164
Serveur
Clients
thread 1
client 1
thread 2
client 2
thread n
client n
165
}//Main
public static void incrmente(){
// augmente le compteur de threads
// lecture compteur
int valeur=cptrThreads;
// suivi
Console.Out.WriteLine("A "+DateTime.Now.ToString("hh:mm:ss")+ ", le thread
"+Thread.CurrentThread.Name+" a lu la valeur du compteur : " +cptrThreads);
// attente
Thread.Sleep(1000);
// incrmentation compteur
cptrThreads=valeur+1;
// suivi
Console.Out.WriteLine("A "+DateTime.Now.ToString("hh:mm:ss")+ ", le thread
"+Thread.CurrentThread.Name+" a crit la valeur du compteur : " +cptrThreads);
}//incrmente
}//classe
Nous ne nous attarderons pas sur la partie gnration de threads dj tudie. Intressons-nous plutt la mthode incrmente,
utilise par chaque thread pour incrmenter le compteur statique cptrThreads.
1. le compteur est lu
2. le thread s'arrte 1 s. Il perd donc le processeur
3. 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 sont les suivants :
E:\data\serge\MSNET\c#\threads\5>comptage
A 09:15:58, le thread 0 a lu la valeur du
A 09:15:58, le thread 1 a lu la valeur du
A 09:15:58, le thread 2 a lu la valeur du
A 09:15:58, le thread 3 a lu la valeur du
A 09:15:58, le thread 4 a lu la valeur du
A 09:15:59, le thread 0 a crit la valeur
A 09:15:59, le thread 1 a crit la valeur
A 09:15:59, le thread 2 a crit la valeur
A 09:15:59, le thread 3 a crit la valeur
A 09:15:59, le thread 4 a crit la valeur
Nombre de threads gnrs : 1
5
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
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.
166
// 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 plusieurs outils pour assurer l'entre unitaire dans les
sections critiques de code. Nous utiliserons la classe Mutex :
// from module 'c:\winnt\microsoft.net\framework\v1.0.3705\mscorlib.dll'
public sealed class System.Threading.Mutex :
System.Threading.WaitHandle,
IDisposable
{
// Constructors
public Mutex();
public Mutex(bool initiallyOwned);
public Mutex(bool initiallyOwned, string name);
public Mutex(bool initiallyOwned, string name, ref Boolean createdNew);
// Properties
public IntPtr Handle { virtual get; virtual set; }
// Methods
public virtual void Close();
public virtual System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType);
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public virtual object GetLifetimeService();
public Type GetType();
public virtual object InitializeLifetimeService();
public void ReleaseMutex();
public virtual string ToString();
public virtual bool WaitOne();
public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);
public virtual bool WaitOne(TimeSpan timeout, bool exitContext);
} // end of System.Threading.Mutex
public void
ReleaseMutex()
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 faut bien sr ne jamais oublier de librer un Mutex devenu inutile pour qu'un autre thread puisse entrer
dans la section critique, sinon les threads en attente d'un Mutex jamais libr n'auront jamais accs au processeur. Par ailleurs, il faut
viter la situation d'interblocage (deadlock) 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.
Les threads d'excution
167
Si nous mettons en pratique ce que nous venons de voir sur l'exemple prcdent, notre application devient la suivante :
// utilisation de threads
using System;
using System.Threading;
public class thread1{
// variables de classe
static int cptrThreads=0;
// compteur de threads
static Mutex autorisation; // autorisation d'accs une section critique
//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.Error.WriteLine(syntaxe);
// arrt
Environment.Exit(1);
}//if
// vrification qualit de l'argument
int nbThreads=0;
try{
nbThreads=int.Parse(args[0]);
if(nbThreads<1 || nbThreads>nbMaxThreads)
throw new Exception();
}catch{
// erreur
Console.Error.WriteLine("Nombre de threads incorrect (entre 1 et 100)");
// fin
Environment.Exit(2);
}//catch
// initialisation de l'autorisation d'accs une section critique
autorisation=new Mutex();
// cration et gnration des threads
Thread[] threads=new Thread[nbThreads];
for(int i=0;i<nbThreads;i++){
// cration
threads[i]=new Thread(new ThreadStart(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.Out.WriteLine("Nombre de threads gnrs : " +cptrThreads);
}//Main
public static void incrmente(){
// augmente le compteur de threads
// on demande l'autorisation d'entrer dans la secton critique
autorisation.WaitOne();
// lecture compteur
int valeur=cptrThreads;
// suivi
Console.Out.WriteLine("A "+DateTime.Now.ToString("hh:mm:ss")+ ", le thread
"+Thread.CurrentThread.Name+" a lu la valeur du compteur : " +cptrThreads);
// attente
Thread.Sleep(1000);
// incrmentation compteur
cptrThreads=valeur+1;
// suivi
Console.Out.WriteLine("A "+DateTime.Now.ToString("hh:mm:ss")+ ", le thread
"+Thread.CurrentThread.Name+" a crit la valeur du compteur : " +cptrThreads);
// on rend l'autorisation d'accs
autorisation.ReleaseMutex();
}//incrmente
}//classe
5
compteur : 0
du compteur : 1
compteur : 1
du compteur : 2
compteur : 2
168
A 10:11:40, le thread 2 a
A 10:11:40, le thread 3 a
A 10:11:41, le thread 3 a
A 10:11:41, le thread 4 a
A 10:11:42, le thread 4 a
Nombre de threads gnrs
crit
lu la
crit
lu la
crit
: 5
la valeur
valeur du
la valeur
valeur du
la valeur
du compteur : 3
compteur : 3
du compteur : 4
compteur : 4
du compteur : 5
On a un tableau dans lequel des processus viennent dposer des donnes (les producteurs) et d'autres viennent les lire (les
consommateurs).
Les producteurs sont gaux entre-eux mais exclusifs : un seul producteur la fois peut dposer ses donnes dans le tableau.
Les consommateurs sont gaux entre-eux mais exclusifs : un seul lecteur la fois peut lire les donnes dposes dans le tableau.
Un consommateur ne peut lire les donnes du tableau que lorsqu'un producteur en a dpos dedans et un producteur ne peut
dposer de nouvelles donnes dans le tableau que lorsque celles qui y sont ont t consommes.
Ce type d'vnement est analogue un boolen mais vite des attentes actives ou semi-actives. Ainsi le droit d'criture est contrl
par un boolen peutEcrire, un producteur avant d'crire excutera un code du genre :
while(peutEcrire==false);
// attente active
ou
while(peutEcrire==false){ // attente semi-active
Thread.Sleep(100); // attente de 100ms
}//while
Dans la premire mthode, le thread mobilise inutilement le processeur. Dans la seconde, il vrifie l'tat du boolen peutEcrire toutes
les 100 ms. La classe AutoResetEvent permet encore d'amliorer les choses : le thread va demander tre rveill lorsque l'vnement
qu'il attend se sera produit :
AutoEvent peutEcrire=new AutoResetEvent(false);
// peutEcrire=false;
....
peutEcrire.WaitOne(); // le thread attend que l'vt peutEcrire passe vrai
L'opration
Les threads d'excution
169
// peutEcrire=false;
excute par un thread fait que celui-ci passe si le boolen peutEcrire est vrai ou sinon est bloqu jusqu' ce qu'il devienne vrai. Un
autre thread le passera vrai par l'opration peutEcrire.Set() ou faux par l'opration peutEcrire.Reset().
Le programme de producteurs-consommateurs est le suivant :
// utilisation de threads lecteurs et crivains
// illustre l'utilisation simultane de ressources partages et de synchronisation
using System;
using System.Threading;
public class thread1{
// variables de classe
static int[] data=new int[5];
// ressource partage entre threads lecteur et threads crivain
static Mutex lecteur;
// variable de synchronisation pour lire le tableau
static Mutex crivain;
// variable de synchronisation pour crire dans le tableau
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=3;
// initialisation des drapeaux
peutLire=new AutoResetEvent(false); // on ne peut pas encore lire
peutEcrire=new AutoResetEvent(true); // on peut dj crire
// initialisation des variables de synchronisation
lecteur=new Mutex();
// synchronise les lecteurs
crivain=new Mutex(); // synchronise les crivains
// cration des threads lecteurs
Thread[] lecteurs=new Thread[nbThreads];
for(int i=0;i<nbThreads;i++){
// cration
lecteurs[i]=new Thread(new ThreadStart(lire));
lecteurs[i].Name=""+i;
// lancement
lecteurs[i].Start();
}//for
// cration des threads crivains
Thread[] crivains=new Thread[nbThreads];
for(int i=0;i<nbThreads;i++){
// cration
crivains[i]=new Thread(new ThreadStart(crire));
crivains[i].Name=""+i;
// lancement
crivains[i].Start();
}//for
//fin de main
Console.Out.WriteLine("fin de Main...");
}//Main
// lire le contenu du tableau
public static void lire(){
// section critique
lecteur.WaitOne();
// un seul lecteur peut passer
peutLire.WaitOne(); // on doit pouvoir lire
// lecture tableau
for(int i=0;i<data.Length;i++){
//attente 1 s
Thread.Sleep(1000);
// affichage
Console.Out.WriteLine(DateTime.Now.ToString("hh:mm:ss") + " : Le lecteur "+
Thread.CurrentThread.Name+ " a lu le nombre " +data[i]);
}//for
// on ne peut plus lire
peutLire.Reset();
// on peut crire
peutEcrire.Set();
// fin de section critique
lecteur.ReleaseMutex();
}//lire
// crire dans le tableau
170
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 crire
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
171
8. Programmation TCP-IP
8.1 Gnralits
8.1.1 Les protocoles de l'Internet
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 est bon 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
|-------------------------------------|
|
Application
|
|-------------------------------------|
|
Prsentation
|
|-------------------------------------|
|
Session
|
|-------------------------------------|
|
Transport
|
|-------------------------------------|
|
Rseau
|
|-------------------------------------|
|
Liaison
|
|-------------------------------------|
|
Physique
|
|-------------------------------------|
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
Programmation TCP-IP
172
transmettre la couche du dessous : la couche Prsentation. L'application n'a donc connatre que les rgles d'interfaage avec la
couche Prsentation.
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 :
7
6
5
4
3
2
1
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
+------------------------------------+
+---------------------------+
^
+-->------->------>-----+
Liaison
donnes
Rseau
Transport
Session
Prsentation
Application
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).
de 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.
Programmation TCP-IP
173
+-----------------------+
Application
+-----------------------
Prsentation
+-----------------------
Session
+-----------------------
Transport
+-----------------------
Rseau
+-----------------------
Liaison
+-----------------------
Physique
+-----------------------+
7
6
5
4
3
2
1
+-------------------------------------------------------+
DNS
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.
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.
IP (Internet Protocol)
Programmation TCP-IP
ICMP
(Internet Control Message Protocol)
ARP
(Address Resolution Protocol)
RARP
(Reverse Address Resolution Protocol)
Couches Transport/Session
Dans cette couche, on trouve les protocoles suivants :
TCP (Transmission Control Protocol)
UDP (User Datagram Protocol)
Couches Application/Prsentation/Session
On trouve ici divers protocoles :
TELNET
TFTP (Trivial
Protocol)
File
SMTP (Simple Mail Transfer permet l'change de messages entre utilisateurs du rseau
protocol)
DNS (Domain Name System)
XDR
(eXternal
Representation)
Data 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
Programmation TCP-IP
175
+----------------+
+---------------------------+
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.
.
.
.
.
.
.
.
.
.
. 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.
176
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
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
+-------------------------------------------------------------------------------+
L'adresse du rseau est sur 2 octets (14 bits exactement) ainsi que celle du noeud. On peut donc avoir 214 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
+-------------------------------------------------------------------------------+
L'adresse rseau est sur 3 octets (moins 3 bits) et l'adresse du noeud sur 1 octet. On peut donc avoir 221 rseaux de classe C
comportant jusqu' 256 noeuds.
Programmation TCP-IP
177
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.
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
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.
Programmation TCP-IP
178
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.
8.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 :
.
.
.
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
+------------+
179
.
.
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.
+--------------------------------------------------------------------------------+
Ces datagrammes seront encapsuls dans des paquets IP, puis dans des trames physiques.
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 TCP-IP
180
.
.
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.
181
.
.
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.
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.
8.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
La plupart des mthodes offertes sont statiques. Regardons celles qui nous intressent :
GetHostByAddress (string
address)
Programmation TCP-IP
rend une adresse IPHostEntry partir d'une adresse IP sous la forme "I1.I2.I3.I4". Lance une
exception si la machine address ne peut tre trouve.
182
GetHostByName(string name)
rend une adresse IPHostEntry partir d'un nom de machine. Lance une exception si la
machine name ne peut tre trouve.
rend le nom de la machine sur laquelle s'excute le programme qui joue cette instruction
string GetHostName()
string [] Aliases
string HostName
liste des adresses IP d'une machine. Si une adresse IP dsigne une et une seule machine physique,
une machine physique peut elle avoir plusieurs adresses IP. Ce sera le cas si elle a plusieurs cartes
rseau qui la connectent des rseaux diffrents.
liste des alias d'une machine, pouvant tre dsigne par un nom principal et des alias
le nom de la machine si elle en a un
De faon interne, une adresse IP est code sous la forme d'un entier long dans la proprit :
public long Address { get; set; }
Elle peut tre transforme en chane I1.I2.I3.I4 avec la mthode ToString(). Inversement, on peut obtenir un objet IPAddress partir
d'une chane I1.I2.I3.I4 avec la mthode statique IPAddress.Parse("I1.I2.I3.I4").
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 :
E:\data\serge\MSNET\c#\sockets\1>address1
Machine Locale=tahe
Machine recherche (fin pour arrter) : istia.univ-angers.fr
Machine : istia.univ-angers.fr
Adresses IP : 193.49.146.171
Machine recherche (fin pour arrter) : 193.49.146.171
Machine : istia.istia.univ-angers.fr
Adresses IP : 193.49.146.171
Alias : 171.146.49.193.in-addr.arpa
Machine recherche (fin pour arrter) : www.ibm.com
Machine : www.ibm.com
Adresses IP : 129.42.17.99,129.42.18.99,129.42.19.99,129.42.16.99
Programmation TCP-IP
183
Programmation TCP-IP
184
Machine B
Port PA
Port PB
Rseau
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 :
"
"
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
Programmation TCP-IP
185
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.
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
Programmation TCP-IP
186
public
public
public
public
int
int
int
int
// Methods
public void Close();
public void Connect(System.Net.IPAddress address, int port);
public void Connect(string hostname, int port);
public void Connect(System.Net.IPEndPoint remoteEP);
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public System.Net.Sockets.NetworkStream GetStream();
public Type GetType();
public virtual string ToString();
} // end of System.Net.Sockets.TcpClient
Les constructeurs, mthodes et proprits qui nous intressent sont les suivants :
constructeur TcpClient(string
hostname, int port)
void Close()
GetStream()
cre une liaison tcp avec le serveur 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
ferme la connexion au serveur Tcp
obtient un flux de lecture et d'criture vers le serveur. C'est ce flux qui permet les
changes client-serveur.
socket);
socket, System.IO.FileAccess access);
socket, System.IO.FileAccess access, bool ownsSocket);
socket, bool ownsSocket);
// Properties
public bool CanRead { virtual get; }
public bool CanSeek { virtual get; }
public bool CanWrite { virtual get; }
public bool DataAvailable { virtual get; }
public long Length { virtual get; }
public long Position { virtual get; virtual set; }
// Methods
public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback,
object state);
public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback,
object state);
public virtual void Close();
public virtual System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType);
public virtual int EndRead(IAsyncResult asyncResult);
public virtual void EndWrite(IAsyncResult asyncResult);
public virtual bool Equals(object obj);
public virtual void Flush();
public virtual int GetHashCode();
public virtual object GetLifetimeService();
public Type GetType();
public virtual object InitializeLifetimeService();
public virtual int Read(byte[] buffer, int offset, int size);
public virtual int ReadByte();
public virtual long Seek(long offset, System.IO.SeekOrigin origin);
public virtual void SetLength(long value);
public virtual string ToString();
public virtual void Write(byte[] buffer, int offset, int size);
public virtual void WriteByte(byte value);
} // end of System.Net.Sockets.NetworkStream
La classe NetworkStream 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 il est intressant d'utiliser des objets StreamReader et StreamWriter pour lire
et crire ces lignes dans le flux rseau. Lorsque deux machines communiquent, il y a chaque bout de la liaison un objet TcpClient.
La mthode GetStream de cet objet permet d'avoir accs au flux rseau (NetworkStream) qui lie les deux machines. Ainsi si une
machine M1 a tabli une liaison avec une machine M2 l'aide d'un objet TcpClient client1 qu'elles changent des lignes de texte, elle
pourra crer ses flux de lecture et criture de la faon suivante :
Programmation TCP-IP
187
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.
Pour envoyer une ligne de texte la machine M2, on crira :
client1.WriteLine("un texte");
}
// c'est fini
client.Close();
} catch(Exception
e){
catch
// on gre l'exception
.
}
Programmation TCP-IP
188
Les constructeurs, mthodes et proprits qui nous intressent sont les suivants :
constructeur TcpListener(int port)
TcpClient AcceptTcpClient()
void Start()
void Stop()
cre un service TCP qui va attendre (listen) les demandes des clients sur un port pass
en paramtre (port) appel port d'coute.
accepte la demande d'un client. Rend comme rsultat un objet TcpClient associ un
autre port, appel port de service.
lance l'coute des demandes clients
arrte d'couter les demandes clients
// constructeur
public Service(TcpClient liaisonClient){
this.liaisonClient=liaisonClient;
}// constructeur
// run
public void Run(){
StreamReader in=null; // flux d'entre de la liaison tcp
StreamWriter out=null; // flux de sortie de la liaison tcp
try{
// on cre les flux d'entre-sortie
in=new StreamReader(liaisonClient.getStream()));
out=new StreamWriter(liaisonClient.getStream());
// boucle demande - rponse
bool fini=false;
String demande;
String rponse;
while (! fini){
// on lit la demande du clien
demande=in.ReadLine();
// on la traite
// on prpare la rponse
rponse=
// on l'envoie au client
out.WriteLine(rponse);
}//while
// c'est fini
liaisonClient.Close();
} catch(Exception e){
// on gre l'exception
Programmation TCP-IP
189
.
}// try
} // run
}//classe
8.4 Exemples
8.4.1 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 :
// appel : serveurEcho port
// serveur d'cho
// renvoie au client la ligne que celui-ci lui a envoye
using
using
using
using
System.Net.Sockets;
System;
System.IO;
System.Threading;
Programmation TCP-IP
190
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}// fin classe serveurEcho
// ------------------------------------------------------// assure le service un client du serveur d'cho
public class traiteClientEcho{
private
private
private
private
TcpClient liaisonClient;
// liaison avec le client
int numClient;
// n de client
StreamReader IN;
// flux d'entre
StreamWriter OUT;
// flux de sortie
// constructeur
public traiteClientEcho(TcpClient liaisonClient, int numClient){
this.liaisonClient=liaisonClient;
this.numClient=numClient;
}//constructeur
// mthode run
public void Run(){
// rend le service au client
Console.Out.WriteLine("Dbut de service au client "+numClient);
try{
// flux d'entre
IN=new StreamReader(liaisonClient.GetStream());
// flux de sortie
OUT=new StreamWriter(liaisonClient.GetStream());
OUT.AutoFlush=true;
// boucle lecture demande/criture rponse
string demande=null,reponse=null;
while ((demande=IN.ReadLine())!=null){
// suivi
Console.Out.WriteLine("Client " + numClient + " : " + demande);
// le service s'arrte lorsque le client envoie une marque de fin de fichier
reponse="["+demande+"]";
OUT.WriteLine(reponse);
// le service s'arrte lorsque le client envoie "fin"
if(demande.Trim().ToLower()=="fin") break;
}// fin while
// fin liaison
liaisonClient.Close();
} catch (Exception e){
erreur("Erreur lors de la fermeture de la liaison client ("+e+")",2);
}// fin try
// fin du service
Console.Out.WriteLine("Fin de service au client "+numClient);
}// fin run
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}// fin class
Programmation TCP-IP
191
if(args.Length != 2)
erreur(syntaxe,1);
// on note le nom du serveur
string nomServeur=args[0];
// le port doit tre entier >0
int port=0;
bool erreurPort=false;
Exception E=null;
try{
port=int.Parse(args[1]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
// on peut travailler
TcpClient client=null; // le client
StreamReader IN=null; // le flux de lecture du client
StreamWriter OUT=null; // le flux d'criture du client
string demande=null;
// demande du client
string rponse=null;
// rponse du serveur
try{
// on se connecte au service officiant sur le port P de la machine M
client=new TcpClient(nomServeur,port);
// on cre les flux d'entre-sortie du client TCP
IN=new StreamReader(client.GetStream());
OUT=new StreamWriter(client.GetStream());
OUT.AutoFlush=true;
// boucle demande - rponse
while (true){
// la demande vient du clavier
Console.Out.Write("demande (fin pour arrter) : ");
demande=Console.In.ReadLine();
// on l'envoie au serveur
OUT.WriteLine(demande);
// on lit la rponse du serveur
rponse=IN.ReadLine();
// on traite la rponse
Console.Out.WriteLine("Rponse : " + rponse);
// fini ?
if(demande.Trim().ToLower()=="fin") break;
}//while
// c'est fini
client.Close();
} catch(Exception e){
// on gre l'exception
erreur(e.Message,3);
}//catch
}//main
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
La structure de ce client est conforme l'architecture gnrale des 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 1 on a les rsultats suivants :
E:\data\serge\MSNET\c#\sockets\serveurEcho>clientEcho localhost 100
demande (fin pour arrter) : ligne1
Rponse : [ligne1]
demande (fin pour arrter) : ligne1B
Rponse : [ligne1B]
demande (fin pour arrter) : ligne1C
Rponse : [ligne1C]
demande (fin pour arrter) : fin
Rponse : [fin]
Programmation TCP-IP
192
demande
Rponse
demande
Rponse
demande
Rponse
Programmation TCP-IP
193
ligne3
.
<-- 250
quit
<-- 221
[fin du
fin
[fin du
<-- 220 istia.univ-angers.fr ESMTP Sendmail 8.11.6/8.9.3; Mon, 13 May 2002 08:37:26 +0200
certains services ont une commande help donnant des indications sur les commandes utilisables avec le service. Ici ce n'est
pas le cas. Les commandes SMTP utilises dans l'exemple sont les suivantes :
o mail from: expditeur, pour indiquer l'adresse lectronique de l'expditeur du message
o rcpt to: destinataire, pour indiquer l'adresse lectronique du destinataire du message. S'il y a plusieurs destinataires,
on r-met autant de fois que ncessaire la commande rcpt to: pour chacun des destinataires.
o data qui signale au serveur SMTP qu'on va envoyer le message. Comme indiqu dans la rponse du serveur,
celui-ci est une suite de lignes termine par une ligne contenant le seul caractre point. Un message peut avoir
des enttes spars du corps du message par une ligne vide. Dans notre exemple, nous avons mis un sujet avec le
mot cl Subject:
une fois le message envoy, on peut indiquer au serveur qu'on a termin avec la commande quit. Le serveur ferme alors la
connexion rseau.Le thread de lecture peut dtecter cet vnement et s'arrter.
l'utilisateur tape alors fin au clavier pour arrter galement le thread de lecture des commandes tapes au clavier.
On remarquera que le service SMTP ne peut dtecter si un expditeur est valide ou non. Aussi ne peut-on jamais faire confiance au
champ from d'un message. Ici l'expditeur machin@univ-angers.fr n'existait pas.
Ce client tcp gnrique peut nous permettre de dcouvrir le protocole de dialogue de services internet et partir de l construire des
classes spcialises pour des clients de ces services. Dcouvrons le protocole de dialogue du service POP (Post Office Protocol) qui
permet de retrouver ses mls stocks sur un serveur. Il travaille sur le port 110.
E:\data\serge\MSNET\c#\rseau\client tcp gnrique>cltgen istia.univ-angers.fr 110
Commandes :
<-- +OK Qpopper (version 4.0.3) at istia.univ-angers.fr starting.
help
<-- -ERR Unknown command: "help".
user st
<-- +OK Password required for st.
pass monpassword
<-- +OK st has 157 visible messages (0 hidden) in 11755927 octets.
list
<-- +OK 157 visible messages (11755927 octets)
<-- 1 892847
<-- 2 171661
...
<-- 156 2843
<-- 157 2796
<-- .
retr 157
<-- +OK 2796 octets
<-- Received: from lagaffe.univ-angers.fr (lagaffe.univ-angers.fr [193.49.144.1])
<-by istia.univ-angers.fr (8.11.6/8.9.3) with ESMTP id g4D6wZs26600;
<-Mon, 13 May 2002 08:58:35 +0200
<-- Received: from jaume ([193.49.146.242])
<-by lagaffe.univ-angers.fr (8.11.1/8.11.2/GeO20000215) with SMTP id g4D6wSd37691;
Programmation TCP-IP
194
user login, o on donne son login sur la machine qui dtient nos mls
pass password, o on donne le mot de passe associ au login prcdent
list, pour avoir la liste des messages sous la forme numro, taille en octets
retr i, pour lire le message n i
quit, pour arrter le dialogue.
Dcouvrons maintenant le protocole de dialogue entre un client et un serveur Web qui lui travaille habituellement sur le port 80 :
E:\data\serge\MSNET\c#\rseau\client tcp gnrique>cltgen istia.univ-angers.fr 80
Commandes :
GET /index.html HTTP/1.0
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
<-- Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
<-- Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
<-- ETag: "23432-2bf3-3c60f0ca"
<-- Accept-Ranges: bytes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
<-<-- <html>
<-<-- <head>
<-- <meta http-equiv="Content-Type"
<-- content="text/html; charset=iso-8859-1">
<-- <meta name="GENERATOR" content="Microsoft FrontPage Express 2.0">
<-- <title>Bienvenue a l'ISTIA - Universite d'Angers</title>
<-- </head>
....
<-- face="Verdana"> - Dernire mise jour le <b>10 janvier 2002</b></font></p>
<-- </body>
<-- </html>
<-[fin du thread de lecture des rponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
qui demande au serveur l'URL /index.html et indique qu'il travaille avec le protocole HTTP version 1.0. La version la plus rcente
de ce protocole est 1.1. L'exemple montre que le serveur a rpondu en renvoyant le contenu du fichier index.html puis qu'il a ferm
la connexion puisqu'on voit le thread de lecture des rponses se terminer. Avant d'envoyer le contenu du fichier index.html, le
serveur web a envoy une srie d'enttes termine par une ligne vide :
<-- HTTP/1.1 200 OK
<-- Date: Mon, 13 May 2002 07:30:58 GMT
Programmation TCP-IP
195
<-<-<-<-<-<-<-<-<--
La ligne <html> est la premire ligne du fichier /index.html. Ce qui prcde s'appelle des enttes HTTP (HyperText Transfer
Protocol). Nous n'allons pas dtailler ici ces enttes mais on se rappellera que notre client gnrique y donne accs, ce qui peut tre
utile pour les comprendre. La premire ligne par exemple :
<-- HTTP/1.1 200 OK
indique que le serveur Web contact comprend le protocole HTTP/1.1 et qu'il a bien trouv le fichier demand (200 OK), 200
tant un code de rponse HTTP. Les lignes
<-- Content-Length: 11251
<-- Connection: close
<-- Content-Type: text/html
disent au client qu'il va recevoir 11251 octets reprsentant du texte HTML (HyperText Markup Language) et qu' la fin de l'envoi,
la connexion sera ferme.
On a donc l un client tcp trs pratique. En fait, ce client existe dj sur les machines o il s'appelle telnet mais il tait intressant de
l'crire nous-mmes. Le programme du client tcp gnrique est le suivant :
// espaces de noms
using System;
using System.Net.Sockets;
using System.IO;
using System.Threading;
public class clientTcpGnrique{
//
//
//
//
//
//
//
//
Programmation TCP-IP
196
}//catch
// on cre les threads de lecture/criture
Thread thReceive=new Thread(new ThreadStart(new clientReceive(client).Run));
Thread thSend=new Thread(new ThreadStart(new clientSend(client).Run));
// on lance l'excution des deux threads
thSend.Start();
thReceive.Start();
// fin thread Main
return;
}// Main
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
public class clientSend{
// classe charge de lire des commandes tapes au clavier
// et de les envoyer un serveur via un client tcp pass au constructeur
TcpClient client; // le client tcp
// constructeur
public clientSend(TcpClient client){
// on note le client tcp
this.client=client;
}//constructeur
// mthode Run du thread
public void Run(){
// donnes locales
StreamWriter OUT=null; // flux d'criture rseau
string commande=null; // commande lue au clavier
// gestion des erreurs
try{
// cration du flux d'criture rseau
OUT=new StreamWriter(client.GetStream());
OUT.AutoFlush=true;
// boucle saisie-envoi des commandes
Console.Out.WriteLine("Commandes : ");
while(true){
// lecture commande tape au clavier
commande=Console.In.ReadLine().Trim();
// fini ?
if (commande.ToLower()=="fin") break;
// envoi commande au serveur
OUT.WriteLine(commande);
// commande suivante
}//while
}catch(Exception ex){
// erreur
Console.Error.WriteLine("L'erreur suivante s'est produite : " + ex.Message);
}//catch
// fin - on ferme les flux
try{
OUT.Close();client.Close();
}catch{}
// on signale la fin du thread
Console.Out.WriteLine("[fin du thread d'envoi des commandes au serveur]");
}//run
}//classe
public class clientReceive{
// classe charge de lire les lignes de texte destines un
// client tcp pass au constructeur
TcpClient client; // le client tcp
// constructeur
public clientReceive(TcpClient client){
// on note le client tcp
this.client=client;
}//constructeur
// mthode Run du thread
public void Run(){
// donnes locales
StreamReader IN=null;
string rponse=null;
Programmation TCP-IP
197
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 :
E:\data\serge\MSNET\c#\rseau\serveur tcp gnrique>srvgen 100
Serveur gnrique lanc sur le port 100
Thread de lecture des rponses du serveur au client 1 lanc
1 : Thread de lecture des demandes du client 1 lanc
<-- commande 1 du client 1
rponse 1 au client 1
1 : <-- commande 2 du client 1
rponse 2 au client 1
1 : [fin du Thread de lecture des demandes du client 1]
fin
[fin du Thread de lecture des rponses du serveur au client 1]
Les lignes commenant par <-- sont celles envoyes du client au serveur. Les lignes N : sont les lignes envoyes du serveur au client
n N. Le serveur ci-dessus est encore actif alors que le client 1 est termin. On lance un second client pour le mme serveur :
E:\data\serge\MSNET\c#\rseau\client tcp gnrique>cltgen localhost 100
Commandes :
commande 3 du client 2
Programmation TCP-IP
198
Simulons maintenant un serveur web en lanant notre serveur gnrique sur le port 88 :
E:\data\serge\MSNET\c#\rseau\serveur tcp gnrique>srvgen 88
Serveur gnrique lanc sur le port 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 :
On dcouvre ainsi les enttes HTTP envoys par le navigateur. Cela nous permet de dcouvrir peu peu le protocole HTTP. Lors
d'un prcdent exemple, nous avions cr un client Web qui n'envoyait que la seule commande GET. Cela avait t suffisant. On
voit ici que le navigateur envoie d'autres informations au serveur. Elles ont pour but d'indiquer au serveur quel type de client il a en
face de lui. On voit aussi que les enttes HTTP se terminent par une ligne vide.
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 :
<-<-<-<-<--
HTTP/1.1 200 OK
Date: Mon, 13 May 2002 07:30:58 GMT
Server: Apache/1.3.12 (Unix) (Red Hat/Linux) PHP/3.0.15 mod_perl/1.21
Last-Modified: Wed, 06 Feb 2002 09:00:58 GMT
ETag: "23432-2bf3-3c60f0ca"
Programmation TCP-IP
199
<-<-<-<-<-<--
Accept-Ranges: bytes
Content-Length: 11251
Connection: close
Content-Type: text/html
<html>
Les lignes commenant par 2 : sont envoyes du serveur au client n 2. La commande fin clt la connexion du serveur au client.
Nous nous sommes limits dans notre rponse aux enttes HTTP suivants :
HTTP/1.1 200 OK
2 : Server: serveur tcp generique
2 : Connection: close
2 : Content-Type: text/html
2 :
Nous ne donnons pas la taille du fichier 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 la suivante :
2
2
2
2
2
2
2
2
: <html>
:
<head><title>Serveur generique</title></head>
:
<body>
:
<center>
:
<h2>Reponse du serveur generique</h2>
:
</center>
:
</body>
: </html>
L'utilisateur ferme ensuite la connexion au client en tapant la commande fin. Le navigateur sait alors que la rponse du serveur est
termine et peut alors l'afficher :
Programmation TCP-IP
200
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
:
<-- GET /exemple.html HTTP/1.1
<-- Host: localhost:88
<-- Connection: close
Si ci-dessus, nous remplaons GET par HEAD, le serveur ne nous enverra que les enttes HTTP et pas la page HTML.
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. Nous lanons le serveur IIS puis le client web sur la mme machine :
E:\data\serge\MSNET\c#\rseau\clientweb>clientweb http://localhost HEAD
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 09:23:37 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=HMFNCCMDECBJJBPPBHAOAJNP; path=/
Cache-control: private
E:\data\serge\MSNET\c#\rseau\clientweb>
La rponse
HTTP/1.1 302 Object moved
signifie que la page demande a chang de place (donc d'URL). La nouvelle URL est donne par l'entte Location:
Location: /IISSamples/Default/welcome.htm
201
Nous obtenons le mme rsultat qu'avec HEAD avec de plus le corps de la page HTML. Le programme est le suivant :
// espaces de noms
using System;
using System.Net.Sockets;
using System.IO;
public class clientWeb{
// demande une URL
// affiche le contenu de celle-ci l'cran
public static void Main(string[] args){
// syntaxe
const string syntaxe="pg URI GET/HEAD";
// nombre d'arguments
if(args.Length != 2)
erreur(syntaxe,1);
// on note l'URI demande
string URIstring=args[0];
string commande=args[1].ToUpper();
// vrification validit de l'URI
Uri uri=null;
try{
uri=new Uri(URIstring);
}catch (Exception ex){
// URI incorrecte
erreur("L'erreur suivante s'est produite : " + ex.Message,2);
}//catch
// vrification de la commande
if(commande!="GET" && commande!="HEAD"){
// commande incorrecte
erreur("Le second paramtre doit tre GET ou HEAD",3);
}
// on peut travailler
TcpClient client=null; // le client
StreamReader IN=null; // le flux de lecture du client
StreamWriter OUT=null; // le flux d'criture du client
string rponse=null;
// rponse du serveur
try{
// on se connecte au serveur
client=new TcpClient(uri.Host,uri.Port);
// on cre les flux d'entre-sortie du client TCP
IN=new StreamReader(client.GetStream());
OUT=new StreamWriter(client.GetStream());
OUT.AutoFlush=true;
// on demande l'URL - envoi des enttes HTTP
OUT.WriteLine(commande+" " + uri.PathAndQuery+ " HTTP/1.1");
OUT.WriteLine("Host: " + uri.Host+":"+uri.Port);
OUT.WriteLine("Connection: close");
OUT.WriteLine();
// on lit la rponse
while((rponse=IN.ReadLine())!=null){
// on traite la rponse
Console.Out.WriteLine(rponse);
}//while
// c'est fini
client.Close();
} catch(Exception e){
// on gre l'exception
erreur(e.Message,4);
}//catch
}//main
// affichage des erreurs
public static void erreur(string msg, int exitCode){
Programmation TCP-IP
202
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
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. Un objet Uri est construit partir de la chane URIstring
reue en paramtre :
Uri uri=null;
try{
uri=new Uri(URIstring);
}catch (Exception ex){
// URI incorrecte
erreur("L'erreur suivante s'est produite : " + ex.Message,2);
}//catch
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/cheminPageHTML?param1=val1;param2=val2;... on aura :
uri.Host=serveur, uri.Port=port, uri.Path=cheminPageHTML, uri.Query=param1=val1;param2=val2;..., uri.pathAndQuery=
cheminPageHTML?param1=val1;param2=val2;..., uri.Scheme=http.
il lit la premire ligne des enttes HTTP envoys par le serveur pour vrifier si on y trouve la chane 302 Object moved qui
signale une redirection
il lit les enttes suivants. S'il y a redirection, il recherche la ligne Location: url qui donne la nouvelle URL de la page
demande et note cette URL.
il affiche le reste de la rponse du serveur. S'il y a redirection, les tapes 1 3 sont rptes avec la nouvelle URL. Le
programme n'accepte pas plus d'une redirection. Cette limite fait l'objet d'une constante qui peut tre modifie.
Voici un exemple :
E:\data\serge\MSNET\c#\rseau\clientweb>clientweb2 http://localhost GET
HTTP/1.1 302 Object moved
Server: Microsoft-IIS/5.0
Date: Mon, 13 May 2002 11:38:55 GMT
Connection: close
Location: /IISSamples/Default/welcome.htm
Content-Length: 189
Content-Type: text/html
Set-Cookie: ASPSESSIONIDGQQQGUUY=PDGNCCMDNCAOFDMPHCJNPBAI; path=/
Cache-control: private
<head><title>L'objet a chang d'emplacement</title></head>
<body><h1>L'objet a chang d'emplacement</h1>Cet objet peut tre trouv <a HREF="/IISSamples/Default/we
lcome.htm">ici</a>.</body>
<--Redirection vers l'URL http://localhost:80/IISSamples/Default/welcome.htm-->
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Connection: close
Date: Mon, 13 May 2002 11:38:55 GMT
Content-Type: text/html
Accept-Ranges: bytes
Last-Modified: Mon, 16 Feb 1998 21:16:22 GMT
ETag: "0174e21203bbd1:978"
Content-Length: 4781
<html>
<head>
<title>Bienvenue dans le Serveur Web personnel</title>
</head>
....
</body>
Programmation TCP-IP
203
</html>
// le client
// le flux de lecture du client
// le flux d'criture du client
// rponse du serveur
// pas plus d'une redirection accepte
// nombre de redirections en cours
// 1re ligne de la rponse
// indique s'il y a redirection ou non
// la chane URI d'une ventuelle redirection
Programmation TCP-IP
204
Console.Out.WriteLine(rponse);
// s'il y a redirection, on recherche l'entte Location
if(redir && ! locationFound){
// on compare la ligne 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;
}//if
}//if
// entte suivant
}//while
// lignes suivantes de la rponse
Console.Out.WriteLine(rponse);
while((rponse=IN.ReadLine())!=null){
// on affiche la rponse
Console.Out.WriteLine(rponse);
}//while
// on ferme la connexion
client.Close();
// a-t-on fini ?
if ( ! locationFound || nbRedirs>nbRedirsMax)
break;
// il y a une redirection oprer - on construit la nouvelle Uri
URIstring=uri.Scheme+"://"+uri.Host+":"+uri.Port+locationString;
uri=new Uri(URIstring);
// suivi
Console.Out.WriteLine("\n<--Redirection vers l'URL "+URIstring+"-->\n");
}//while
} catch(Exception e){
// on gre l'exception
erreur(e.Message,4);
}//catch
}//main
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
// constructeur 2
public impt(string DSNimpots, string Timpots, string colLimites, string colCoeffR, string colCoeffN){
// initialise les trois tableaux limites, coeffR, coeffN partir
// du contenu de la table Timpots de la base ODBC DSNimpots
// colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
// peut lancer une exception
Programmation TCP-IP
205
Paramtres du
impt=22506 F
Paramtres du
impt=33388 F
Paramtres du
impt=16400 F
Paramtres du
impt=50082 F
Paramtres du
impt=22506 F
calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :o 2 200000
calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :n 2 200000
calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :o 3 200000
calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :n 3 300000
calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :n 3 200000
Ici le programme de test et l'objet impt taient sur la mme machine. Nous nous proposons de mettre le programme de test et
l'objet impt sur des machines diffrentes. Nous aurons une application client-serveur o l'objet impt distant sera le serveur. La
nouvelle classe s'appelle ServeurImpots et est drive de la classe impt :
using
using
using
using
using
System.Net.Sockets;
System;
System.IO;
System.Threading;
System.Text.RegularExpressions;
// constructeur
public ServeurImpots(int portEcoute,string DSNimpots, string Timpots, string colLimites, string
colCoeffR, string colCoeffN)
: base(DSNimpots, Timpots, colLimites, colCoeffR, colCoeffN) {
// on note le port d'coute
this.portEcoute=portEcoute;
// pour l'instant inactif
actif=false;
// cre et lance un thread de lecture des commandes tapes au clavier
// le serveur sera gr partir de ces commandes
new Thread(new ThreadStart(admin)).Start();
}//ServeurImpots
Le seul paramtre nouveau dans le constructeur est le port d'coute des demandes des clients. Les autres paramtres sont passs
directement la classe de base impt. Le serveur d'impts est contrl par des commandes tapes au clavier. Aussi cre-t-on un
thread pour lire ces commandes. Il y en aura deux possibles : start pour lancer le service, stop pour l'arrter dfinitivement. La
mthode admin qui gre ces commandes est la suivante :
public void admin(){
// lit les commandes d'administration du serveur tapes au clavier
// dans une boucle sans fin
string commande=null;
while(true){
// invite
Console.Out.Write("Serveur d'impts>");
// lecture commande
commande=Console.In.ReadLine().Trim().ToLower();
// excution commande
if(commande=="start"){
// actif ?
if(actif){
//erreur
Console.Out.WriteLine("Le serveur est dj actif");
// on continue
continue;
}//if
// on lance le service d'coute
new Thread(new ThreadStart(ecoute)).Start();
}//if
else if(commande=="stop"){
// fin de tous les threads d'excution
Environment.Exit(0);
}//if
else {
// erreur
Console.Out.WriteLine("Commande incorrecte. Utilisez (start,stop)");
}//if
}//while
}//admin
Si la commande tape au clavier est start, un thread d'coute des demandes clients est lanc. Si la commande tape est stop, tous les
threads sont arrts. Le thread d'coute excute la mthode ecoute :
public void ecoute(){
// thread d'coute des demandes des clients
// on cre le service d'coute
TcpListener ecoute=null;
Programmation TCP-IP
206
try{
// on cre le service
ecoute=new TcpListener(portEcoute);
// on le lance
ecoute.Start();
// suivi
Console.Out.WriteLine("Serveur d'cho lanc sur le port " + portEcoute);
// boucle de service
TcpClient liaisonClient=null;
while (true){ // boucle infinie
// attente d'un client
liaisonClient=ecoute.AcceptTcpClient();
// le service est assur par une autre tche
new Thread(new ThreadStart(new traiteClientImpots(liaisonClient,this).Run)).Start();
// on retourne l'coute des demandes
}// fin while
}catch(Exception ex){
// on signale l'erreur
erreur("L'erreur suivante s'est produite : " + ex.Message,3);
}//catch
}//thread d'coute
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
On retrouve un serveur tcp classique coutant sur le port portEcoute. Les demandes des clients sont traites par la mthode Run d'un
objet auquel on passe deux paramtres :
1. l'objet TcpClient qui va permettre d'atteindre le client
2. l'objet impt this qui va donner accs la mthode this.calculer de calcul de l'impt.
// ------------------------------------------------------// assure le service un client du serveur d'impts
public class traiteClientImpots{
private
private
private
private
TcpClient liaisonClient;
// liaison avec le client
StreamReader IN;
// flux d'entre
StreamWriter OUT;
// flux de sortie
impt objImpt;
// objet Impt
// constructeur
public traiteClientImpots(TcpClient liaisonClient,impt objImpt){
this.liaisonClient=liaisonClient;
this.objImpt=objImpt;
}//constructeur
La mthode Run traite les demandes des clients. Celles-ci peuvent avoir deux formes :
1. calcul mari(o/n) nbEnfants salaireAnnuel
2. fincalculs
La forme 1 permet le calcul d'un impt, la forme 2 clt la liaison client-serveur.
// mthode Run
public void Run(){
// rend le service au client
try{
// flux d'entre
IN=new StreamReader(liaisonClient.GetStream());
// flux de sortie
OUT=new StreamWriter(liaisonClient.GetStream());
OUT.AutoFlush=true;
// envoi d'un msg de bienvenue au client
OUT.WriteLine("Bienvenue sur le serveur d'impts");
// boucle lecture demande/criture rponse
string demande=null;
string[] champs=null; // les lments de la demande
string commande=null; // la commande du client : calcul ou fincalculs
while ((demande=IN.ReadLine())!=null){
// on dcompose la demande en champs
champs=Regex.Split(demande.Trim().ToLower(),@"\s+");
// deux demandes acceptes : calcul et fincalculs
commande=champs[0];
if(commande!="calcul" && commande!="fincalculs"){
// erreur client
OUT.WriteLine("Commande incorrecte. Utilisez (calcul,fincalculs).");
// commande suivante
continue;
Programmation TCP-IP
207
}//if
if(commande=="calcul") calculerImpt(champs);
if(commande=="fincalculs"){
// msg d'au-revoir au client
OUT.WriteLine("Au revoir...");
// libration des ressources
try{ OUT.Close();IN.Close();liaisonClient.Close();}
catch{}
// fin
return;
}//if
//demande suivante
}//while
}catch (Exception e){
erreur("L'erreur suivante s'est produite ("+e+")",2);
}// fin try
}// fin Run
Le calcul de l'impt est effectu par la mthode calculerImpt qui reoit en paramtre le tableau des champs de la demande faite par le
client. La validit de la demande est vrifie et ventuellement l'impt calcul et renvoy au client.
// calcul d'impts
public void calculerImpt(string[] champs){
// traite la demande : calcul mari nbEnfants salaireAnnuel
// dcompose en champs dans le tableau champs
string mari=null;
int nbEnfants=0;
int salaireAnnuel=0;
// validit des arguments
try{
// il faut au moins 4 champs
if(champs.Length!=4) throw new Exception();
// mari
mari=champs[1];
if (mari!="o" && mari !="n") throw new Exception();
// enfants
nbEnfants=int.Parse(champs[2]);
// salaire
salaireAnnuel=int.Parse(champs[3]);
}catch{
// erreur de format
OUT.WriteLine(" syntaxe : calcul mari(O/N) nbEnfants salaireAnnuel");
// fini
return;
}//if
// on peut calculer l'impt
long impot=objImpt.calculer(mari=="o",nbEnfants,salaireAnnuel);
// on envoie la rponse au client
OUT.WriteLine(""+impot);
}//calculer
o impots.dll contient le code de la classe impt. Un programme de test pourrait tre le suivant :
// appel : serveurImpots port dsnImpots Timpots colLimites colCoeffR colCoeffN
using System;
using System.IO;
public class testServeurImpots{
public const string syntaxe="Syntaxe : pg port dsnImpots Timpots colLimites colCoeffR colCoeffN";
// programme principal
public static void Main (string[] args){
// il faut6 arguments
if(args.Length != 6)
erreur(syntaxe,1);
// le port doit tre entier >0
int port=0;
bool erreurPort=false;
Exception E=null;
try{
port=int.Parse(args[0]);
}catch(Exception e){
E=e;
erreurPort=true;
}
erreurPort=erreurPort || port <=0;
if(erreurPort)
erreur(syntaxe+"\n"+"Port incorrect ("+E+")",2);
Programmation TCP-IP
208
On passe au programme de test les donnes ncessaires la construction d'un objet ServeurImpots et partir de l il cre cet objet. Ce
programme de test est compil par :
csc /r:serveurimpots.dll /r:impots.dll testServeurImpots.cs
La ligne
dos>testserveurimpots 124 mysql-impots timpots limites coeffr coeffn
cre un objet ServeurImpots qui n'coute pas encore les demandes des clients. C'est la commande start tape au clavier qui lance cette
coute. La commande stop arrte le serveur. Utilisons maintenant un client. Nous utiliserons le client gnrique cr
prcdemment. Le serveur est lanc :
E:\data\serge\MSNET\c#\impots\serveur>testserveurimpots 124 mysql-impots timpots limites coeffr coeffn
Serveur d'impts>start
Serveur d'impts>Serveur d'cho lanc sur le port 124
On voit que le client a bien rcupr le message de bienvenue du serveur. On envoie d'autres commandes :
x
<-- Commande incorrecte. Utilisez (calcul,fincalculs).
calcul
<-- syntaxe : calcul mari(O/N) nbEnfants salaireAnnuel
calcul o 2 200000
<-- 22506
calcul n 2 200000
<-- 33388
fincalculs
<-- Au revoir...
[fin du thread de lecture des rponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
Programmation TCP-IP
209
9. Services Web
9.1 Introduction
Nous avons prsent dans le chapitre prcdent plusieurs applications client-serveur tcp-ip. Dans la mesure o les clients et le
serveur changent des lignes de texte, ils peuvent tre crits en n'importe quel langage. Le client doit simplement connatre le
protocole de dialogue attendu par le serveur. Les services Web sont des applications serveur tcp-ip prsentant les caractristiques
suivantes :
Elles sont hberges par des serveurs web et le protocole d'changes client-serveur est donc HTTP (HyperText Transport
Protocol), un protocole au-dessus de TCP-IP.
Le service Web a un protocole de dialogue standard quelque soit le service assur. Un service Web offre divers services S1,
S2, .., Sn. Chacun d'eux attend des paramtres fournis par le client et rend celui-ci un rsultat. Pour chaque service, le
client a besoin de savoir :
o le nom exact du service Si
o la liste des paramtres qu'il faut lui fournir et leur type
o le type de rsultat retourn par le service
Une fois, ces lments connus, le dialogue client-serveur suit le mme format quelque soit le service web
interrog. L'criture des clients est ainsi normalise.
Pour des raisons de scurit vis vis des attaques venant de l'internet, beaucoup d'organisations ont des rseaux privs et
n'ouvrent sur Internet que certains ports de leurs serveurs : essentiellement le port 80 du service web. Tous les autres ports
sont verrouills. Aussi les applications client-serveur telles que prsentes dans le chapitre prcdent sont-elles construites
au sein du rseau priv (intranet) et ne sont en gnral pas accessibles de l'extrieur. Loger un service au sein d'un serveur
web le rend accessible toute la communaut internet.
Le service Web peut tre modlis comme un objet distant. Les services offerts deviennent alors des mthodes de cet
objet. Un client peut avoir accs cet objet distant comme s'il tait local. Cela cache toute la partie communication rseau
et permet de construire un client indpendant de cette couche. Si celle-ci vient changer, le client n'a pas tre modifi.
C'est l un norme avantage et probablement le principal atout des services Web.
Comme pour les applications client-serveur tcp-ip prsentes dans le chapitre prcdent, le client et le serveur peuvent tre
crits dans un langage quelconque. Ils changent des lignes de texte. Celles-ci comportent deux parties :
o les enttes ncessaires au protocole HTTP
o le corps du message. Pour une rponse du serveur au client, celui-ci est au format XML (eXtensible Markup
Language). Pour une demande du client au serveur, le corps du message peut avoir plusieurs formes dont XML.
La demande XML du client peut avoir un format particulier appel SOAP (Simple Object Access Protocol).
Dans ce cas, la rponse du serveur suit aussi le format SOAP.
Conclusion
210
[WebMethod]
public double soustraire(double a, double b){
return a-b;
}//soustraire
[WebMethod]
public double multiplier(double a, double b){
return a*b;
}//multiplier
[WebMethod]
public double diviser(double a, double b){
return a/b;
}//diviser
[WebMethod]
public double[] toutfaire(double a, double b){
return new double[] {a+b,a-b,a*b,a/b};
}//toutfaire
}//classe
Ce code n'est pas destin tre compil directement par le compilateur C#. Ce qui explique la prsence d'une directive que ne peut
comprendre C# :
<%@ WebService Language=C# class=operations %>
Le code-source ci-dessus est en fait destin au serveur Web IIS et non au compilateur C#. La directive indique que le code qui suit :
est un service Web
crit en C#
que la classe l'implmentant s'appelle operations
La classe operations ressemble une classe C# avec cependant quelques points noter :
les mthodes sont prcdes d'un attribut [WebMethod] qui indique au compilateur les mthodes qui doivent tre
"publies" c.a.d. rendues disponibles au client. Une mthode non prcde de cet attribut serait invisible aux clients
distants. Ce pourrait tre une mthode interne utilise par d'autres mthodes mais pas destine tre publie.
la classe drive de la classe WebService dfinie dans l'espace de noms System.Web.Services. Cet hritage n'est pas toujours
obligatoire. Dans cet exemple notamment on pourrait s'en passer.
la classe elle-mme est prcde d'un attribut [WebService(Namespace="st.istia.univ-angers.fr")] destin donner
un espace de noms au service web. Un vendeur de classes donne un espace de noms ses classes afin de leur donner un
nom unique et viter ainsi des conflits avec des classes d'autres vendeurs qui pourraient porter le mme nom. Pour les
services Web, c'est pareil. Chaque service web doit pouvoir tre identifi par un nom unique, ici par st.istia.univangers.fr.
nous n'avons pas dfini de constructeur. Il est possible d'en dfinir mais la documentation consulte ne dit pas comment
utiliser un tel constructeur.
Le code source prcdent n'est pas destin directement au compilateur C# mais au serveur Web IIS. Il doit porter le suffixe .asmx
et sauvegard dans l'arborescence du serveur Web. Ici nous le sauvegardons sous le nom operations.asmx dans le dossier
c:\inetpub\wwwroot\st\operations :
E:\data\serge\MSNET\c#\webservices\clientSOAP>dir c:\inetpub\wwwroot\st\operations
14/05/2002 17:14
549 operations.asmx
On rappelle que c:\inetpub\wwwroot est la racine des documents web dlivrs par le serveur IIS. Demandons le document prcdent
avec un navigateur. L'URl demander est http://localhost/st/operations/operations.asmx.
Conclusion
211
Nous obtenons un document Web avec un lien pour chacune des mthodes dfinies dans le service web operations. Suivons le lien
ajouter :
La page obtenue nous propose de tester la mthode ajouter en lui fournissant les deux arguments a et b dont elle a besoin. Rappelons
la dfinition de la mthode ajouter :
[WebMethod]
public double ajouter(double a, double b){
return a+b;
}//ajouter
On notera que la page a repris les noms des arguments a et b utiliss dans la dfinition de la mthode. On utilise le bouton Appeler
et on obtient la rponse suivante dans une fentre spare du navigateur :
Conclusion
212
On peut remarquer que le navigateur (ici IE) n'a pas reu du code HTML mais du code XML. Par ailleurs on voit que la rponse a
t demande l'URL : http://localhost/st/operations/operations.asmx/ajouter?a=15&b=30. Si nous modifions directement les valeurs
de a et b dans l'URL pour qu'elle devienne http://localhost/st/operations/operations.asmx/ajouter?a=-15&b=17, nous obtenons le
rsultat suivant :
Nous commenons discerner une mthode pour avoir accs une fonction F d'un service web S : on demande l'URL
http://urlServiceWeb/fonction?param1=val1¶m2=val2.... Essayons dans l'URL ci-dessus de remplacer ajouter par soustraire qui est
l'une des fonctions dfinies dans le service Web operations. Nous obtenons le rsultat suivant :
Conclusion
213
Nous savons maintenant comment interroger un service web et obtenir sa rponse. En fait il existe plusieurs mthodes pour faire
cela. Revenons l'URL du service :
Conclusion
214
Dans page ci-dessus, sont exposes (mais non reprsentes dans la copie d'cran ci-dessus) trois mthodes pour interroger la
fonction ajouter du service web :
Conclusion
215
Ces trois mthodes d'accs aux fonctions d'un service web sont appeles respectivement : HTTP-GET, HTTP-POST et SOAP.
Nous les examinons maintenant l'une aprs l'autre.
Le client HTTP doit mettre au minimum les deux commandes HTTP suivantes :
GET /st/operations/operations.asmx/ajouter?a=[a]&b=[b] HTTP/1.1
Host: localhost
o [a] et [b] doivent tre remplaces par les valeurs de a et b. Le serveur web enverra la rponse suivante :
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: [longueur]
<?xml version="1.0" encoding="utf-8"?>
<double xmlns="st.istia.univ-angers.fr">[rsultat]</double>
o [longueur] est le nombre de caractres envoys par le serveur aprs la ligne vide qui suit les enttes HTTP et [rsultat] est le
rsultat de la fonction ajouter. Vrifions cela avec notre client gnrique dfini dans le chapitre prcdent :
E:\data\serge\MSNET\c#\webservices\clientGET>cltgen localhost 80
Conclusion
216
Commandes :
GET http://localhost/st/operations/operations.asmx/ajouter?a=10&b=20 HTTP/1.1
Connection: close
Host: localhost:80
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 13:04:35 GMT
<-- Connection: close
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 91
<-<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">30</double>
[fin du thread de lecture des rponses du serveur]
fin
[fin du thread d'envoi des commandes au serveur]
le client s'est connect au port 80 de la machine localhost c'est dire au serveur Web IIS.
il a envoy les enttes HTTP ncessaires pour demander d'ajouter 10 20. On a rajout l'entte Connection: close pour
demander au serveur de fermer la connexion aprs avoir envoy la rponse. Cela est ncessaire ici. Si on ne le dit pas, par
dfaut le serveur va garder la connexion ouverte. Or sa rponse est une suite de lignes de texte dont la dernire n'est pas
termine par une marque de fin de ligne. Or notre client TCP gnrique lit des lignes de texte termines par la marque de
fin de ligne avec la mthode ReadLine. Si le serveur ne ferme pas la connexion aprs envoi de la dernire ligne, le client est
bloqu parce qu'il attend une marque de fin de ligne qui n'existe pas. Si le serveur ferme la connexion, la mthode ReadLine
du client se termine et le client ne reste pas bloqu.
le serveur a envoy davantage d'enttes qu'attendus mais le format de la rponse XML est bien celui qui tait prvu.
Conclusion
217
Ensuite, le client lit les commandes tapes au clavier et les excute. Celles-ci sont au format :
fonction a b
o fonction est la fonction du service web appele (ajouter, soustraire, multiplier, diviser) et a et b les valeurs sur lesquelles va
oprer cette fonction. Par exemple :
ajouter 3 4
A partir de l, le client va faire la requte HTTP ncessaire au serveur Web et obtenir une rponse. Les changes client-serveur sont
dupliqus l'cran pour une meilleure comprhension du processus :
-->
-->
-->
-->
<-<-<-<-<-<-<-<-<--
On retrouve ci-dessus l'change dj rencontr avec le client tcp gnrique une diffrence prs : l'entte HTTP Connection:
Keep-Alive demande au serveur de ne pas fermer la connexion. Celle-ci reste donc ouverte pour l'opration suivante du client qui
n'a donc pas besoin de se reconnecter de nouveau au serveur. Cela l'oblige cependant utiliser une autre mthode que ReadLine()
pour lire la rponse du serveur puisqu'on sait que celle-ci est une suite de lignes dont la dernire n'est pas termine par une marque
de fin de ligne. Une fois toute la rponse du serveur obtenue, le client l'analyse pour y trouver le rsultat de l'opration demande et
l'afficher :
[rsultat=7]
Conclusion
218
// syntaxe
const string syntaxe="pg URI";
string[] fonctions={"ajouter","soustraire","multiplier","diviser"};
// nombre d'arguments
if(args.Length != 1)
erreur(syntaxe,1);
// on note l'URI demande
string URIstring=args[0];
// on se connecte au serveur
Uri uri=null;
// l'URI du service web
TcpClient client=null; // la liaison tcp du client avec le serveur
StreamReader IN=null; // le flux de lecture du client
StreamWriter OUT=null; // le flux d'criture du client
try{
// connexion au serveur
uri=new Uri(URIstring);
client=new TcpClient(uri.Host,uri.Port);
// on cre les flux d'entre-sortie du client TCP
IN=new StreamReader(client.GetStream());
OUT=new StreamWriter(client.GetStream());
OUT.AutoFlush=true;
}catch (Exception ex){
// URI incorrecte ou autre problme
erreur("L'erreur suivante s'est produite : " + ex.Message,2);
}//catch
// cration d'un dictionnaire des fonctions du service web
Hashtable dicoFonctions=new Hashtable();
for (int i=0;i<fonctions.Length;i++){
dicoFonctions.Add(fonctions[i],true);
}//for
// les demandes de l'utilisateur sont tapes au clavier
// sous la forme fonction a b
// elles se terminent avec la commande fin
string commande=null;
string[] champs=null;
string fonction=null;
string a,b;
//
//
//
//
// invite l'utilisateur
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b");
// gestion des erreurs
try{
// boucle de saisie des commandes tapes au clavier
while(true){
// lecture commande
commande=Console.In.ReadLine().Trim().ToLower();
// fini ?
if(commande==null || commande=="fin") break;
// dcomposition de la commande en champs
champs=Regex.Split(commande,@"\s+");
try{
// il faut trois champs
if(champs.Length!=3)throw new Exception();
// le champ 0 doit tre une fonction reconnue
fonction=champs[0];
if(! dicoFonctions.ContainsKey(fonction)) throw new Exception();
// le champ 1 doit tre un nombre valide
a=champs[1];
double.Parse(a);
// le champ 2 doit tre un nombre valide
b=champs[2];
double.Parse(b);
}catch {
// commande invalide
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b");
// on recommence
continue;
}//catch
// on fait la demande au service web
executeFonction(IN,OUT,uri,fonction,a,b);
// demande suivante
}//while
// fin des demandes
}catch(Exception e){
Console.Out.WriteLine("L'erreur suivante s'est produite : " + e.Message);
}//catch
// fin liaison client-serveur
try{
IN.Close();OUT.Close();client.Close();
}catch{}
}//Main
Conclusion
219
....
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
On a l des choses dj rencontres plusieurs fois et qui ne ncessitent pas de commentaires particuliers. Examinons maintenant le
code de la mthode executeFonction o rsident les nouveauts :
// executeFonction
public static void executeFonction(StreamReader IN, StreamWriter OUT, Uri uri, string fonction, string
a, string b){
// excute fonction(a,b) sur le service web d'URI uri
// les changes client-serveur se font via les flux IN et OUT
// le rsultat de la fonction est dans la ligne
// <double xmlns="st.istia.univ-angers.fr">double</double>
// envoye par le serveur
// construction du tableau des enttes HTTP envoyer
string[] entetes=new string[4];
entetes[0]="GET " + uri.AbsolutePath+ "/"+fonction+"?a="+a+"&b="+b+" HTTP/1.1";
entetes[1]="Host: " + uri.Host+":"+uri.Port;
entetes[2]="Connection: Keep-Alive";
entetes[3]="";
// on envoie les enttes HTTP au serveur
for(int i=0;i<entetes.Length;i++){
// envoi au serveur
OUT.WriteLine(entetes[i]);
// cho cran
Console.Out.WriteLine("--> "+entetes[i]);
}//for
// construction de l'expression rgulire permettant de retrouver la taille de la rponse
// dans le flux de la rponse du serveur web
string modleLength=@"^Content-Length: (.+?)\s*$";
Regex RegexLength=new Regex(modleLength);
Match MatchLength=null;
int longueur=0;
// on lit tous les enttes de la rponse du serveur Web
// on mmorise la valeur de la ligne Content-Length
string ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
// cho cran
Console.Out.WriteLine("<-- " + ligne);
// Content-Length ?
MatchLength=RegexLength.Match(ligne);
if(MatchLength.Success){
longueur=int.Parse(MatchLength.Groups[1].Value);
}//if
}//while
// cho dernire ligne
Console.Out.WriteLine("<--");
// construction de l'expression rgulire permettant de retrouver le rsultat
// dans le flux de la rponse du serveur web
string modle=@"<double xmlns="+"\""+@"st\.istia\.univ-angers\.fr"+"\""+@">(.+?)</double>";
Regex ModleRsultat=new Regex(modle);
Match MatchRsultat=null;
// on lit le reste de la rponse du serveur web
char[] chrRponse=new char[longueur];
IN.Read(chrRponse,0,longueur);
string strRponse=new String(chrRponse);
// on dcompose la rponse en lignes de texte
string[] lignes=Regex.Split(strRponse,"\n");
// on parcourt les lignes de texte la recherche du rsultat
string strRsultat="?"; // rsultat de la fonction
for(int i=0;i<lignes.Length;i++){
// suivi
Console.Out.WriteLine("<-- "+lignes[i]);
// comparaison ligne courante au modle
MatchRsultat=ModleRsultat.Match(lignes[i]);
// a-t-on trouv ?
if(MatchRsultat.Success){
// on note le rsultat
strRsultat=MatchRsultat.Groups[1].Value;
}
}//ligne suivante
// on affiche le rsultat
Console.Out.WriteLine("[rsultat="+strRsultat+"]\n");
}//executeFonction
Conclusion
220
Le client commence par envoyer les enttes HTTP de sa demande au serveur Web :
// construction du tableau des enttes HTTP envoyer
string[] entetes=new string[4];
entetes[0]="GET " + uri.AbsolutePath+ "/"+fonction+"?a="+a+"&b="+b+" HTTP/1.1";
entetes[1]="Host: " + uri.Host+":"+uri.Port;
entetes[2]="Connection: Keep-Alive";
entetes[3]="";
// on envoie les enttes HTTP au serveur
for(int i=0;i<entetes.Length;i++){
// envoi au serveur
OUT.WriteLine(entetes[i]);
// cho cran
Console.Out.WriteLine("--> "+entetes[i]);
}//for
Le serveur ayant reu cette demande calcule sa rponse et l'envoie. Celle-ci est compose de deux parties :
1. des enttes HTTP termins par une ligne vide
2. la rponse au format XML
<-<-<-<-<-<-<-<-<--
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Wed, 15 May 2002 13:18:24 GMT
Cache-Control: private, max-age=0
Content-Type: text/xml; charset=utf-8
Content-Length: 90
<?xml version="1.0" encoding="utf-8"?>
<double xmlns="st.istia.univ-angers.fr">7</double>
Dans un premier temps, le client lit les enttes HTTP pour y trouver la ligne Content-Length et rcuprer la taille de la rponse XML
(ici 90). Celle-ci est rcupre au moyen d'une expression rgulire. On aurait pu faire autrement et sans doute de faon plus
efficace.
// construction de l'expression rgulire permettant de retrouver la taille de la rponse
// dans le flux de la rponse du serveur web
string modleLength=@"^Content-Length: (.+?)\s*$";
Regex RegexLength=new Regex(modleLength);
Match MatchLength=null;
int longueur=0;
// on lit tous les enttes de la rponse du serveur Web
// on mmorise la valeur de la ligne Content-Length
string ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
// cho cran
Console.Out.WriteLine("<-- " + ligne);
// Content-Length ?
MatchLength=RegexLength.Match(ligne);
if(MatchLength.Success){
longueur=int.Parse(MatchLength.Groups[1].Value);
}//if
}//while
// cho dernire ligne
Console.Out.WriteLine("<--");
Une fois qu'on a la longueur N de la rponse XML, on n'a plus qu' lire N caractres dans le flux IN de la rponse du serveur. Cette
chane de N caractres est redcompose en lignes de texte pour les besoins du suivi cran. Parmi ces lignes on cherche la ligne du
rsultat :
<-- <double xmlns="st.istia.univ-angers.fr">7</double>
au moyen l encore d'une expression rgulire. Une fois le rsultat trouv, il est affich.
[rsultat=7]
Conclusion
221
On voit que la demande du client change de forme mais que la rponse du client reste la mme. Utilisons de nouveau notre client
tcp gnrique pour vrifier tout cela :
E:\data\serge\MSNET\c#\webservices\clientPOST>cltgen localhost 80
Commandes :
POST /st/operations/operations.asmx/ajouter HTTP/1.1
Host: localhost
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 7
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 13:59:17 GMT
<-a=2&b=3
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 13:59:22 GMT
<-- Connection: close
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
<-- Content-Length: 90
<-<-- <?xml version="1.0" encoding="utf-8"?>
<-- <double xmlns="st.istia.univ-angers.fr">5</double>
[fin du thread de lecture des rponses du serveur]
fin
Conclusion
222
Nous avons ajout l'entte Connection: close pour des raisons dj expliques prcdemment. On peut constater que la rponse du
serveur IIS n'a pas le format attendu. Aussitt aprs avoir reu la ligne vide signalant la fin des enttes HTTP, il envoie une
premire rponse :
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 13:59:17 GMT
<--
Cette rponse forme uniquement d'enttes HTTP indique au client qu'il peut envoyer les 7 caractres qu'il a dit vouloir envoyer.
Ce que nous faisons :
a=2&b=3
Il faut voir ici que notre client tcp envoie plus de 7 caractres puisqu'il les envoie avec une marque de fin de ligne (WriteLine). Ca ne
gne pas le serveur qui des caractres reus ne prendra que les 7 premiers et parce qu'ensuite la connexion est ferme (Connection:
close). Ces caractres en trop auraient t gnants si la connexion tait reste ouverte car alors ils auraient t pris comme venant de
la commande suivante du client. Une fois les paramtres reus, le serveur envoie sa rponse, identique cette fois celle qu'il faisait
au client HTTP-GET :
<-<-<-<-<-<-<-<-<-<--
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Wed, 15 May 2002 13:59:22 GMT
Connection: close
Cache-Control: private, max-age=0
Content-Type: text/xml; charset=utf-8
Content-Length: 90
<?xml version="1.0" encoding="utf-8"?>
<double xmlns="st.istia.univ-angers.fr">5</double>
Conclusion
223
La premire partie du client HTTP-POST est identique celle correspondante du cient HTTP-GET. Seul change le code de la
fonction executeFonction :
// executeFonction
public static void executeFonction(StreamReader IN, StreamWriter OUT, Uri uri, string fonction, string
a, string b){
// excute fonction(a,b) sur le service web d'URI uri
// les changes client-serveur se font via les flux IN et OUT
// le rsultat de la fonction est dans la ligne
// <double xmlns="st.istia.univ-angers.fr">double</double>
// envoye par le serveur
// construction de la chane de requte
string requte="a="+HttpUtility.UrlEncode(a)+"&b="+HttpUtility.UrlEncode(b);
int nbChars=requte.Length;
// construction du tableau des enttes HTTP envoyer
string[] entetes=new string[6];
entetes[0]="POST " + uri.AbsolutePath+ "/"+fonction+" HTTP/1.1";
entetes[1]="Host: " + uri.Host+":"+uri.Port;
entetes[2]="Content-Type: application/x-www-form-urlencoded";
entetes[3]="Content-Length: "+nbChars;
entetes[4]="Connection: Keep-Alive";
entetes[5]="";
// on envoie les enttes HTTP au serveur
for(int i=0;i<entetes.Length;i++){
// envoi au serveur
OUT.WriteLine(entetes[i]);
// cho cran
Console.Out.WriteLine("--> "+entetes[i]);
}//for
// on lit la 1ere rponse du serveur Web HTTP/1.1 100
string ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
//cho
Console.Out.WriteLine("<-- "+ligne);
}//while
//cho dernire ligne
Console.Out.WriteLine("<-- "+ligne);
// envoi paramtres de la requte
OUT.Write(requte);
// echo
Console.Out.WriteLine("--> "+requte);
// construction de l'expression rgulire permettant de retrouver la taille de la rponse XML
// dans le flux de la rponse du serveur web
string modleLength=@"^Content-Length: (.+?)\s*$";
Regex RegexLength=new Regex(modleLength);
Match MatchLength=null;
int longueur=0;
// lecture seconde rponse du serveur web aprs envoi de la requte
// on mmorise la valeur de la ligne Content-Length
ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
// cho cran
Console.Out.WriteLine("<-- " + ligne);
// Content-Length ?
MatchLength=RegexLength.Match(ligne);
if(MatchLength.Success){
longueur=int.Parse(MatchLength.Groups[1].Value);
}//if
}//while
// cho dernire ligne
Console.Out.WriteLine("<--");
// construction de l'expression rgulire permettant de retrouver le rsultat
// dans le flux de la rponse du serveur web
string modle=@"<double xmlns="+"\""+@"st\.istia\.univ-angers\.fr"+"\""+@">(.+?)</double>";
Regex ModleRsultat=new Regex(modle);
Match MatchRsultat=null;
// on lit le reste de la rponse du serveur web
Conclusion
224
Dans l'entte
--> Content-Length: 7
on doit indiquer la taille des paramtres qui seront envoys par le client derrire les enttes HTTP :
--> a=8&b=9
La mthode HttpUtility.UrlEncode(string chaine) transforme certains des caractres de chane en %n1n2 o n1n2 est le code ASCII du
caractre transform. Les caractres viss par cette transformation sont tous les caractres ayant un sens particulier dans une
requte POST (l'espace, le signe =, le signe &, ...). Ici la mthode HttpUtility.UrlEncode est normalement inutile puisque a et b sont
des nombres qui ne contiennent aucun de ces caractres particuliers. Elle est ici employe titre d'exemple. Elle a besoin de l'espace
de noms System.Web.
Une fois que le client a envoy ses enttes HTTP
ajouter 6 7
--> POST /st/operations/operations.asmx/ajouter HTTP/1.1
--> Host: localhost:80
--> Content-Type: application/x-www-form-urlencoded
--> Content-Length: 7
--> Connection: Keep-Alive
-->
Conclusion
225
Une fois cette premire rponse lue, le client doit envoyer ses paramtres :
--> a=8&b=9
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Wed, 15 May 2002 14:11:47 GMT
Cache-Control: private, max-age=0
Content-Type: text/xml; charset=utf-8
Content-Length: 91
<?xml version="1.0" encoding="utf-8"?>
<double xmlns="st.istia.univ-angers.fr">13</double>
Cette rponse tant identique celle faite au client HTTP-GET, le code du client HTTP-POST pour traiter cette rponse est
identique au code correspondant du client HTTP-GET.
Conclusion
226
La demande du client est une demande POST. On va donc retrouver certains des mcanismes du client prcdent. La principale
diffrence est qu'alors que le client HTTP-POST envoyait les paramtres a et b sous la forme
a=A&b=B
le client SOAP les envoie dans un format XML plus complexe :
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>double</a>
<b>double</b>
</ajouter>
</soap:Body>
</soap:Envelope>
Il reoit en retour une rponse XML galement plus complexe que les rponses vues prcdemment :
HTTP/1.1 200 OK
Content-Type: text/xml; charset=utf-8
Content-Length: length
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouterResponse xmlns="st.istia.univ-angers.fr">
<ajouterResult>double</ajouterResult>
</ajouterResponse>
</soap:Body>
</soap:Envelope>
Mme si la demande et la rponse sont plus complexes, il s'agit bien du mme mcanisme HTTP que pour le client HTTP-POST.
L'criture du client SOAP peut tre ainsi calqu sur celle du client HTTP-POST. Voici un exemple d'excution :
E:\data\serge\MSNET\c#\webservices\clientSOAP>clientsoap1 http://localhost/st/operations/operations.
asmx
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a b
ajouter 3 4
--> POST /st/operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 14:39:17 GMT
<---> <?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/20
01/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ajouter xmlns="st.istia.univ-angers.fr">
<a>3</a>
<b>4</b>
</ajouter>
</soap:Body>
</soap:Envelope>
<-- HTTP/1.1 200 OK
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 14:39:17 GMT
<-- Cache-Control: private, max-age=0
<-- Content-Type: text/xml; charset=utf-8
Conclusion
227
L encore, seule la mthode executeFonction change. Le client SOAP envoie les enttes HTTP de sa demande. Ils sont simplement un
peu plus complexes que ceux de HTTP-POST :
ajouter 3 4
--> POST /st/operations/operations.asmx HTTP/1.1
--> Host: localhost:80
--> Content-Type: text/xml; charset=utf-8
--> Content-Length: 321
--> Connection: Keep-Alive
--> SOAPAction: "st.istia.univ-angers.fr/ajouter"
-->
En recevant cette demande, le serveur envoie sa premire rponse que le client affiche :
<-- HTTP/1.1 100 Continue
<-- Server: Microsoft-IIS/5.0
<-- Date: Wed, 15 May 2002 14:39:17 GMT
<--
Le code :
// on lit la 1ere rponse du serveur Web HTTP/1.1 100
string ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
//cho
Console.Out.WriteLine("<-- "+ligne);
}//while
//cho dernire ligne
Console.Out.WriteLine("<-- "+ligne);
Le client va maintenant envoyer ses paramtres au format XML dans quelque chose qu'on appelle une enveloppe SOAP :
Conclusion
228
Le code :
// envoi paramtres de la requte
OUT.Write(requteSOAP);
// echo
Console.Out.WriteLine("--> "+requteSOAP);
Le client affiche l'cran les enttes HTTP reus tout en y cherchant la ligne Content-Length :
// construction de l'expression rgulire permettant de retrouver la taille de la rponse XML
// dans le flux de la rponse du serveur web
string modleLength=@"^Content-Length: (.+?)\s*$";
Regex RegexLength=new Regex(modleLength);
Match MatchLength=null;
int longueur=0;
// lecture seconde rponse du serveur web aprs envoi de la requte
// on mmorise la valeur de la ligne Content-Length
ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
// cho cran
Console.Out.WriteLine("<-- " + ligne);
// Content-Length ?
MatchLength=RegexLength.Match(ligne);
if(MatchLength.Success){
longueur=int.Parse(MatchLength.Groups[1].Value);
}//if
}//while
// cho dernire ligne
Console.Out.WriteLine("<--");
Une fois la taille N de la rponse XML connue, le client lit N caractres dans le flux de la rponse du serveur, dcompose la chane
rcupre en lignes de texte pour les afficher l'cran et y chercher la balise XML du rsultat : <ajouterResult>7</ajouterResult> et
afficher ce dernier :
// construction de l'expression rgulire permettant de retrouver le rsultat
// dans le flux de la rponse du serveur web
string modle="<"+fonction+"Result>(.+?)</"+fonction+"Result>";
Regex ModleRsultat=new Regex(modle);
Match MatchRsultat=null;
// on lit le reste de la rponse du serveur web
char[] chrRponse=new char[longueur];
IN.Read(chrRponse,0,longueur);
string strRponse=new String(chrRponse);
// on dcompose la rponse en lignes de texte
string[] lignes=Regex.Split(strRponse,"\n");
// on parcourt les lignes de texte la recherche du rsultat
string strRsultat="?"; // rsultat de la fonction
for(int i=0;i<lignes.Length;i++){
// suivi
Console.Out.WriteLine("<-- "+lignes[i]);
// comparaison ligne courante au modle
Conclusion
229
MatchRsultat=ModleRsultat.Match(lignes[i]);
// a-t-on trouv ?
if(MatchRsultat.Success){
// on note le rsultat
strRsultat=MatchRsultat.Groups[1].Value;
}
}//ligne suivante
// on affiche le rsultat
Console.Out.WriteLine("[rsultat="+strRsultat+"]\n");
}//executeFonction
Machine serveur
Interface
client-serveur
Service Web
internet
L'application cliente s'adresserait l'interface client-serveur pour faire ses demandes au service web. Celle-ci ferait tous les changes
rseau ncessaires avec le serveur et rendrait le rsultat obtenu l'application cliente. Celle-ci n'aurait plus s'occuper des changes
avec le serveur ce qui faciliterait grandement son criture.
Conclusion
230
}//constructeur
public void Close(){
// fin liaison client-serveur
IN.Close();OUT.Close();client.Close();
}//Close
// executeFonction
public string executeFonction(string fonction, string a, string b){
// excute fonction(a,b) sur le service web d'URI uri
// les changes client-serveur se font via les flux IN et OUT
// le rsultat de la fonction est dans la ligne
// <double xmlns="st.istia.univ-angers.fr">double</double>
// envoye par le serveur
// fonction valide ?
fonction=fonction.Trim().ToLower();
if (! dicoFonctions.ContainsKey(fonction))
return "[fonction ["+fonction+"] indisponible : (ajouter, soustraire,multiplier,diviser)]";
// arguments a et b valides ?
double doubleA=0;
try{
doubleA=double.Parse(a);
}catch{
return "[argument ["+a+"] incorrect (double)]";
}//catch
double doubleB=0;
try{
doubleB=double.Parse(b);
}catch{
return "[argument ["+b+"] incorrect (double)]";
}//catch
// division par zro ?
if(fonction=="diviser" && doubleB==0)
return "[division par zro]";
// construction de la chane de requte SOAP
string requteSOAP="<?xml version="+"\"1.0\" encoding=\"utf-8\"?>\n";
requteSOAP+="<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\"
xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n";
requteSOAP+="<soap:Body>\n";
requteSOAP+="<"+fonction+" xmlns=\"st.istia.univ-angers.fr\">\n";
requteSOAP+="<a>"+a+"</a>\n";
requteSOAP+="<b>"+b+"</b>\n";
requteSOAP+="</"+fonction+">\n";
requteSOAP+="</soap:Body>\n";
requteSOAP+="</soap:Envelope>";
int nbCharsSOAP=requteSOAP.Length;
// construction du tableau des enttes HTTP envoyer
string[] entetes=new string[7];
entetes[0]="POST " + uri.AbsolutePath+" HTTP/1.1";
entetes[1]="Host: " + uri.Host+":"+uri.Port;
entetes[2]="Content-Type: text/xml; charset=utf-8";
entetes[3]="Content-Length: "+nbCharsSOAP;
entetes[4]="Connection: Keep-Alive";
entetes[5]="SOAPAction: \"st.istia.univ-angers.fr/"+fonction+"\"";
entetes[6]="";
// on envoie les enttes HTTP au serveur
for(int i=0;i<entetes.Length;i++){
// envoi au serveur
OUT.WriteLine(entetes[i]);
// cho cran
if (verbose) Console.Out.WriteLine("--> "+entetes[i]);
}//for
// on lit la 1ere rponse du serveur Web HTTP/1.1 100
string ligne=null;
// une ligne du flux de lecture
while((ligne=IN.ReadLine())!=""){
//cho
if (verbose) Console.Out.WriteLine("<-- "+ligne);
}//while
//cho dernire ligne
if (verbose) Console.Out.WriteLine("<-- "+ligne);
// envoi paramtres de la requte
OUT.Write(requteSOAP);
// echo
if (verbose) Console.Out.WriteLine("--> "+requteSOAP);
// construction de l'expression rgulire permettant de retrouver la taille de la rponse XML
// dans le flux de la rponse du serveur web
string modleLength=@"^Content-Length: (.+?)\s*$";
Regex RegexLength=new Regex(modleLength);
Match MatchLength=null;
int longueur=0;
// lecture seconde rponse du serveur web aprs envoi de la requte
// on mmorise la valeur de la ligne Content-Length
ligne=null;
// une ligne du flux de lecture
Conclusion
231
while((ligne=IN.ReadLine())!=""){
// cho cran
if (verbose) Console.Out.WriteLine("<-- " + ligne);
// Content-Length ?
MatchLength=RegexLength.Match(ligne);
if(MatchLength.Success){
longueur=int.Parse(MatchLength.Groups[1].Value);
}//if
}//while
// cho dernire ligne
if (verbose) Console.Out.WriteLine("<--");
// construction de l'expression rgulire permettant de retrouver le rsultat
// dans le flux de la rponse du serveur web
string modle="<"+fonction+"Result>(.+?)</"+fonction+"Result>";
Regex ModleRsultat=new Regex(modle);
Match MatchRsultat=null;
// on lit le reste de la rponse du serveur web
char[] chrRponse=new char[longueur];
IN.Read(chrRponse,0,longueur);
string strRponse=new String(chrRponse);
// on dcompose la rponse en lignes de texte
string[] lignes=Regex.Split(strRponse,"\n");
// on parcourt les lignes de texte la recherche du rsultat
string strRsultat="?"; // rsultat de la fonction
for(int i=0;i<lignes.Length;i++){
// suivi
if (verbose) Console.Out.WriteLine("<-- "+lignes[i]);
// comparaison ligne courante au modle
MatchRsultat=ModleRsultat.Match(lignes[i]);
// a-t-on trouv ?
if(MatchRsultat.Success){
// on note le rsultat
strRsultat=MatchRsultat.Groups[1].Value;
}
}//ligne suivante
// on renvoie le rsultat
return strRsultat;
}//executeFonction
}//classe
Nous ne retrouvons rien de neuf par raport ce qui a t dj vu. Nous avons simplement repris le code du client SOAP tudi et
l'avons ramnag quelque peu pour en faire une classe. Celle-ci a un constructeur et deux mthodes :
// constructeur
public clientSOAP(string uriString, bool verbose){
// executeFonction
public string executeFonction(string fonction, string a, string b){
public void Close(){
232
2.
La mthode ExecuteFonction qui rendait auparavant un type void et affichait le rsultat de la fonction l'cran, rend
maintenant ce rsultat et donc un type string.
cration d'un objet clientSOAP qui va crer la liaison avec le service web
utilisation rpte de la mthode executeFonction
fermeture de la liaison avec le service Web par la mthode Close.
// invite l'utilisateur
Console.Out.WriteLine("Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser] a
b\n");
// gestion des erreurs
try{
// boucle de saisie des commandes tapes au clavier
while(true){
// lecture commande
commande=Console.In.ReadLine().Trim().ToLower();
// fini ?
if(commande==null || commande=="fin") break;
// dcomposition de la commande en champs
champs=Regex.Split(commande,@"\s+");
// il faut trois champs
if(champs.Length!=3){
// commande invalide
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b");
// on recommence
continue;
}//if
// on fait la demande au service web
Console.Out.WriteLine("rsultat="+client.executeFonction(champs[0].Trim().ToLower(),champs[1].Trim(),cha
mps[2].Trim()));
// demande suivante
}//while
// fin des demandes
}catch(Exception e){
Console.Out.WriteLine("L'erreur suivante s'est produite : " + e.Message);
Conclusion
233
}//catch
// fin liaison client-serveur
try{
client.Close();
}catch{}
}//Main
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
La client est maintenant considrablement plus simple et on n'y retrouve aucune communication rseau. Le client admet deux
paramtres :
1. l'URI du service web operations
2. le mot cl facultatif verbose. S'il est prsent, les changes rseau seront affichs l'cran.
Ces deux paramtres sont utiliss pour construire un objet clientSOAP qui va assurer les changes avec le service web.
// on se connecte au service web
clientSOAP client=null;
try{
client=new clientSOAP(args[0],verbose);
}catch(Exception ex){
// erreur de connexion
erreur("L'erreur suivante s'est produite lors de la connexion au service web : "
+ ex.Message,2);
}//catch
Une fois ouverte la connexion avec le service web, le client peut envoyer ses demandes. Celles-ci sont tapes au clavier, analyses
puis envoyes au serveur par appel la mthode executeFonction de l'objet clientSOAP.
// on fait la demande au service web
Console.Out.WriteLine("rsultat="+client.executeFonction(champs[0].Trim().ToLower(),champs[1].Trim(),cha
mps[2].Trim()));
09:08
09:08
6 065 ClientSOAP.cs
7 168 ClientSOAP.dll
Conclusion
234
Conclusion
235
1
2
6
4
5
7
8
9
type
TextBox
Button
Button
ComboBox
TextBox
TextBox
TextBox
Button
TextBox
nom
txtURI
btnOuvrir
btnFermer
cmbFonctions
txtA
txtB
txtRsultat
btnCalculer
txtErreur
rle
l'URI du service web operations
ouvre la liaison avec le service Web
ferme la liaison avec le service Web
la liste des fonction (ajouter, soustraire, multiplier, diviser)
l'argument a des fonctions
l'argument b des fonctions
le rsultat de fonction(a,b)
lance le calcul de fonction(a,b)
affiche un msg d'tat de la liaison
Conclusion
236
Le code de l'application suit. Nous avons omis le code du formulaire qui ne prsente pas d'intrt ici.
public class Form1 : System.Windows.Forms.Form
{
// contrles formulaire
...
// variables globales
clientSOAP client=null;
public Form1()
{
// Required for Windows Form Designer support
InitializeComponent();
// init formulaire
btnOuvrir.Enabled=false;
btnFermer.Enabled=false;
btnCalculer.Enabled=false;
cmbFonctions.SelectedIndex=0;
// client SOAP
}
private void InitializeComponent()
{
...
}
static void Main()
{
Application.Run(new Form1());
}
Conclusion
237
Machine serveur
Intermdiaire
client-serveur
Service Web
internet
La plate-forme .NET pousse cette logique plus loin. Une fois connu le service Web atteindre, nous pouvons gnrer
automatiquement la classe qui va nous servir d'intermdiaire pour atteindre les fonctions du service Web et qui cachera toute la
partie rseau. On appelle cette classe un proxy pour le service web pour lequel elle a t gnre.
Conclusion
238
Comment gnrer la classe proxy d'un service web ? Un service web est toujours accompagn d'un fichier de description au format
XML. Si l'URI de notre service web operations est http://localhost/st/operations/operations.asmx, son fichier de description est
disponible l'URL http://localhost/st/operations/operations.asmx?wsdl comme le montre la copie d'cran suivante :
On a l un fichier XML dcrivant prcisment toutes les fonctions du service web avec pour chacune d'elles le type et le nombre de
paramtres, le type du rsultat. On appelle ce fichier le fichier WSDL du service parce qu'il utilise le langage WSDL (Web Services
Description Language). A partir de ce fichier, une classe proxy peut tre gnre l'aide de l'outil wsdl :
E:\data\serge\MSNET\c#\webservices\clientproxy>wsdl http://localhost/st/operations/operations.asmx?wsdl
Microsoft (R) Web Services Description Language Utility
[Microsoft (R) .NET Framework, Version 1.0.3705.0]
Copyright (C) Microsoft Corporation 1998-2001. All rights reserved.
criture du fichier 'E:\data\serge\MSNET\c#\webservices\clientproxy\operations.cs'.
E:\data\serge\MSNET\c#\webservices\clientproxy>dir
16/05/2002 13:00
6 642 operations.cs
L'outil wsdl gnre un fichier source C# portant le nom de la classe implmentant le service web, ici operations. Examinons une partie
du code gnr :
//-----------------------------------------------------------------------------// <autogenerated>
//
This code was generated by a tool.
//
Runtime Version: 1.0.3705.0
//
//
Changes to this file may cause incorrect behavior and will be lost if
//
the code is regenerated.
// </autogenerated>
//-----------------------------------------------------------------------------//
// Ce
//
using
using
using
using
using
using
/// <remarks/>
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Web.Services.WebServiceBindingAttribute(Name="operationsSoap", Namespace="st.istia.univangers.fr")]
public class operations : System.Web.Services.Protocols.SoapHttpClientProtocol {
/// <remarks/>
public operations() {
this.Url = "http://localhost/st/operations/operations.asmx";
}
/// <remarks/>
Conclusion
239
[System.Web.Services.Protocols.SoapDocumentMethodAttribute("st.istia.univ-angers.fr/ajouter",
RequestNamespace="st.istia.univ-angers.fr", ResponseNamespace="st.istia.univ-angers.fr",
Use=System.Web.Services.Description.SoapBindingUse.Literal,
ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Wrapped)]
public System.Double ajouter(System.Double a, System.Double b) {
object[] results = this.Invoke("ajouter", new object[] {
a,
b});
return ((System.Double)(results[0]));
}
/// <remarks/>
public System.IAsyncResult Beginajouter(System.Double a, System.Double b, System.AsyncCallback
callback, object asyncState) {
return this.BeginInvoke("ajouter", new object[] {
a,
b}, callback, asyncState);
}
/// <remarks/>
public System.Double Endajouter(System.IAsyncResult asyncResult) {
object[] results = this.EndInvoke(asyncResult);
return ((System.Double)(results[0]));
}
....
Ce code est un peu complexe au premier abord. Nous n'avons pas besoin d'en comprendre les dtails pour pouvoir l'utiliser.
Examinons tout d'abord la dclaration de la classe :
public class operations : System.Web.Services.Protocols.SoapHttpClientProtocol {
La classe porte le nom operations du service web pour lequel elle a t construite. Elle drive de la classe SoapHttpClientProtocol :
// from module
'c:\winnt\assembly\gac\system.web.services\1.0.3300.0__b03f5f7f11d50a3a\system.web.services.dll'
public class System.Web.Services.Protocols.SoapHttpClientProtocol :
System.Web.Services.Protocols.HttpWebClientProtocol,
System.ComponentModel.IComponent,
IDisposable
{
// Fields
// Constructors
public SoapHttpClientProtocol();
// Properties
public bool AllowAutoRedirect { get; set; }
public X509CertificateCollection ClientCertificates { get; }
public string ConnectionGroupName { get; set; }
public IContainer Container { get; }
public CookieContainer CookieContainer { get; set; }
public ICredentials Credentials { get; set; }
public bool PreAuthenticate { get; set; }
public IWebProxy Proxy { get; set; }
public Encoding RequestEncoding { get; set; }
public ISite Site { virtual get; virtual set; }
public int Timeout { get; set; }
public string Url { get; set; }
public string UserAgent { get; set; }
// Events
public event EventHandler Disposed;
// Methods
public virtual void Abort();
public virtual System.Runtime.Remoting.ObjRef CreateObjRef(Type requestedType);
public void Discover();
public virtual void Dispose();
public virtual bool Equals(object obj);
public virtual int GetHashCode();
public virtual object GetLifetimeService();
public Type GetType();
public virtual object InitializeLifetimeService();
public virtual string ToString();
} // end of System.Web.Services.Protocols.SoapHttpClientProtocol
Conclusion
240
Le constructeur affecte l'attibut url l'URL du service web associ au proxy. La classe operations ci-dessus ne dfinit pas elle-mme
l'attribut url. Celui-ci est hrit de la classe dont drive le proxy : System.Web.Services.Protocols.SoapHttpClientProtocol. Examinons
maintenant ce qui se rapporte la mthode ajouter :
public System.Double ajouter(System.Double a, System.Double b) {
object[] results = this.Invoke("ajouter", new object[] {
a,
b});
return ((System.Double)(results[0]));
}
On peut constater qu'elle a la mme signature que dans le service Web operations o elle tait dfinie comme suit :
[WebMethod]
public double ajouter(double a, double b){
return a+b;
}//ajouter
La faon dont cette classe dialogue avec le service Web n'apparat pas ici. Ce dialogue est entirement pris en charge par la classe
parent System.Web.Services.Protocols.SoapHttpClientProtocol. On ne trouve dans le proxy que ce qui le diffrencie des autres proxy :
Pour utiliser les mthodes du service web operations, un client n'a besoin que de la classe proxy operations gnre prcdemment.
Compilons cette classe dans un fichier assembly :
E:\data\serge\MSNET\c#\webservices\clientproxy>csc /t:library operations.cs
E:\data\serge\MSNET\c#\webservices\clientproxy>dir
16/05/2002 13:00
6 642 operations.cs
16/05/2002 13:33
7 680 operations.dll
Maintenant crivons un client. Nous reprenons un client console dj utilis plusieurs fois. Il est appel sans paramtres et excute
les demandes tapes au clavier :
E:\data\serge\MSNET\c#\webservices\clientproxy>testclientproxy
Tapez vos commandes au format : [ajouter|soustraire|multiplier|diviser|toutfaire] a b
ajouter 4 5
rsultat=9
soustraire 9 8
rsultat=1
multiplier 10 4
rsultat=40
diviser 6 7
rsultat=0,857142857142857
toutfaire 10 20
rsultats=[30,-10,200,0,5]
diviser 5 0
rsultat=+Infini
fin
Conclusion
241
try{
myOperations=new operations();
}catch(Exception ex){
// erreur de connexion
erreur("L'erreur suivante s'est produite lors de la connexion au proxy dy service web : "
+ ex.Message,2);
}//catch
// les demandes de l'utilisateur sont tapes au clavier
// sous la forme fonction a b
// elles se terminent avec la commande fin
string commande=null;
string[] champs=null;
// invite l'utilisateur
Console.Out.WriteLine("Tapez vos commandes au format :
[ajouter|soustraire|multiplier|diviser|toutfaire] a b\n");
// gestion des erreurs
// boucle de saisie des commandes tapes au clavier
while(true){
// lecture commande
commande=Console.In.ReadLine().Trim().ToLower();
// fini ?
if(commande==null || commande=="fin") break;
// dcomposition de la commande en champs
champs=Regex.Split(commande,@"\s+");
// commande valide ?
string fonction;
double a,b;
try{
// il faut trois champs
if(champs.Length!=3)throw new Exception();
// le champ 0 doit tre une fonction reconnue
fonction=champs[0];
if(! dicoFonctions.ContainsKey(fonction)) throw new Exception();
// le champ 1 doit tre un nombre valide
a=double.Parse(champs[1]);
// le champ 2 doit tre un nombre valide
b=double.Parse(champs[2]);
}catch {
// commande invalide
Console.Out.WriteLine("syntaxe : [ajouter|soustraire|multiplier|diviser] a b");
// on recommence
continue;
}//catch
// on fait la demande au service web
try{
double rsultat;
double[] rsultats;
if(fonction=="ajouter"){
rsultat=myOperations.ajouter(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//ajouter
if(fonction=="soustraire"){
rsultat=myOperations.soustraire(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//soustraire
if(fonction=="multiplier"){
rsultat=myOperations.multiplier(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//multiplier
if(fonction=="diviser"){
rsultat=myOperations.diviser(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//diviser
if(fonction=="toutfaire"){
rsultats=myOperations.toutfaire(a,b);
Console.Out.WriteLine("rsultats=["+rsultats[0]+","+rsultats[1]+","+rsultats[2]+","+rsultats[3]+"]")
;
}//toutfaire
}catch(Exception e){
Console.Out.WriteLine("L'erreur suivante s'est produite : " + e.Message);
}//catch
// demande suivante
}//while
}//Main
// affichage des erreurs
public static void erreur(string msg, int exitCode){
// affichage erreur
System.Console.Error.WriteLine(msg);
// arrt avec erreur
Environment.Exit(exitCode);
}//erreur
}//classe
Nous n'examinons que le code propre l'utilisation de la classe proxy. Tout d'abord un objet proxy operations est cr :
Conclusion
242
Des lignes fonction a b sont tapes au clavier. A partir de ces informations, on appelle les mthodes appropries du proxy :
// on fait la demande au service web
try{
double rsultat;
double[] rsultats;
if(fonction=="ajouter"){
rsultat=myOperations.ajouter(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//ajouter
if(fonction=="soustraire"){
rsultat=myOperations.soustraire(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//soustraire
if(fonction=="multiplier"){
rsultat=myOperations.multiplier(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//multiplier
if(fonction=="diviser"){
rsultat=myOperations.diviser(a,b);
Console.Out.WriteLine("rsultat="+rsultat);
}//diviser
if(fonction=="toutfaire"){
rsultats=myOperations.toutfaire(a,b);
Console.Out.WriteLine("rsultats=["+rsultats[0]+","+rsultats[1]+","+rsultats[2]+","+rsultats[3]+"]")
;
}//toutfaire
}catch(Exception e){
Console.Out.WriteLine("L'erreur suivante s'est produite : " + e.Message);
}//catch
On traite ici pour la premire fois, l'opration toutfaire qui fait les quatre oprations. Elle avait t ignore pour l'instant car elle
envoie un tableau de nombres encapsul dans une enveloppe XML plus complique grer que les rponses XML simples des
autres fonctions ne dlivrant qu'un rsultat. On voit qu'ici avec la classe proxy, utiliser la mthode toutfaire n'est pas plus compliqu
qu'utiliser les autres mthodes.
Comment le service Web rcupre-t-il ces informations ? Lorsque IIS charge un service web, il regarde s'il y a dans le mme dossier
un fichier web.config. Si oui, il le lit. La valeur V d'un paramtre P est obtenue par l'instruction :
string P=ConfigurationSettings.AppSettings["V"];
243
Le service web personne a deux attributs nom et age qui sont initialiss dans son constructeur sans paramtres partir des valeurs lues
dans le fichier de configuration web.config du service personne. Ce fichier est le suivant :
<configuration>
<appSettings>
<add key="nom" value="tintin"/>
<add key="age" value="27"/>
</appSettings>
</configuration>
Le service web a par ailleurs une [WebMethod] id sans paramtres et qui se contente de rendre les attributs nom et age. Le service
est enregistr dans le fichier source personne.asmx qui est plac avec son fichier de configuration dans le dossier
c:\inetpub\wwwroot\st\personne :
E:\data\serge\MSNET\c#\webservices\clientproxy>dir c:\inetpub\wwwroot\st\personne
16/05/2002 14:38
133 web.config
16/05/2002 14:46
521 personne.asmx
Conclusion
244
Nous avons bien rcupr les informations places dans le fichier web.config du service.
System;
System.Data;
Microsoft.Data.Odbc;
System.Collections;
Conclusion
245
Dans le service web, on ne peut utiliser qu'un constructeur sans paramtres. Aussi le constructeur de la classe va-t-il devenir le
suivant :
using System.Configuration;
// constructeur
public impt(){
// initialise les trois tableaux limites, coeffR, coeffN partir
// du contenu de la table Timpots de la base ODBC DSNimpots
// colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
// peut lancer une exception
// on rcupre les paramtres de configuration du service
string DSNimpots=ConfigurationSettings.AppSettings["DSN"];
string Timpots=ConfigurationSettings.AppSettings["TABLE"];
string colLimites=ConfigurationSettings.AppSettings["COL_LIMITES"];
string colCoeffR=ConfigurationSettings.AppSettings["COL_COEFFR"];
string colCoeffN=ConfigurationSettings.AppSettings["COL_COEFFN"];
// on exploite la base de donnes
string connectString="DSN="+DSNimpots+";"; // chane de connexion la base
....
Les cinq paramtres du constructeur de la classe prcdente sont maintenant lus dans le fichier web.config du service. Le code du
fichier source impots.asmx est le suivant. Il reprend la majeure partie du code prcdent. Nous nous sommes contents d'encadrer les
portions de code propres au service web :
<%@ WebService Language=C# class=impt %>
// cration d'un servie web impots
using System;
using System.Data;
using Microsoft.Data.Odbc;
using System.Collections;
using System.Configuration;
using System.Web.Services;
[WebService(Namespace="st.istia.univ-angers.fr")]
public class impt : WebService{
// les donnes ncessaires au calcul de l'impt
// proviennent d'une source extrieure
private decimal[] limites, coeffR, coeffN;
bool OK=false;
Conclusion
246
string errMessage="";
// constructeur
public impt(){
// initialise les trois tableaux limites, coeffR, coeffN partir
// du contenu de la table Timpots de la base ODBC DSNimpots
// colLimites, colCoeffR, colCoeffN sont les trois colonnes de cette table
// peut lancer une exception
// on rcupre les paramtres de configuration du service
string DSNimpots=ConfigurationSettings.AppSettings["DSN"];
string Timpots=ConfigurationSettings.AppSettings["TABLE"];
string colLimites=ConfigurationSettings.AppSettings["COL_LIMITES"];
string colCoeffR=ConfigurationSettings.AppSettings["COL_COEFFR"];
string colCoeffN=ConfigurationSettings.AppSettings["COL_COEFFN"];
// on exploite la base de donnes
string connectString="DSN="+DSNimpots+";"; // chane de connexion la base
...
// on tente d'accder la base de donnes
try{
impotsConn = new OdbcConnection(connectString);
impotsConn.Open();
...
// c'est bon
OK=true;
errMessage="";
}catch(Exception ex){
// erreur
OK=false;
errMessage+="["+ex.Message+"]";
}//catch
}//constructeur
// calcul de l'impt
[WebMethod]
public long calculer(bool mari, int nbEnfants, int salaire){
// calcul du nombre de parts
...
}//calculer
// id
[WebMethod]
public string id(){
// pour voir si tout est OK
return "["+OK+","+errMessage+"]";
}//id
}//classe
Expliquons les quelques modifications faites la classe impt en-dehors de celles ncessaires pour en faire un service web :
la lecture de la base de donnes dans le constructeur peut chouer. Aussi avons-nous ajout deux attributs notre classe et
une mthode :
o le boolen OK est vrai si la base a pu tre lue, faux sinon
o la chane errMessage contient un message d'erreur si la base de donnes n'a pu tre lue.
o la mthode id sans paramtres permet d'obtenir la valeur ces deux attributs.
pour grer l'erreur ventuelle d'accs la base de donnes, la partie du code du constructeur concerne par cet accs a t
entoure d'un try-catch.
Lors d'un premier essai de chargement du service impots, le compilateur a dclar qu'il ne trouvait pas l'espace de noms
Microsoft.Data.Odbc utilis dans la directive :
using Microsoft.Data.Odbc;
247
une copie du fichier microsoft.data.odbc.dll a t place dans le dossier c:\inetpub\wwwroot\bin qui est systmatiquement
explor par le compilateur d'un service web lorsqu'il recherche un "assembly".
D'autres solutions semblent possibles mais n'ont pas t cruses ici. Le fichier de configuration est donc devenu :
<configuration>
<appSettings>
<add key="DSN" value="mysql-impots" />
<add key="TABLE" value="timpots" />
<add key="COL_LIMITES" value="limites" />
<add key="COL_COEFFR" value="coeffr" />
<add key="COL_COEFFN" value="coeffn" />
</appSettings>
<system.web>
<compilation>
<assemblies>
<add assembly="Microsoft.Data.Odbc" />
</assemblies>
</compilation>
</system.web>
</configuration>
Si on suit le lien id :
Le rsultat prcdent affiche les valeurs des attributs OK (true) et errMessage (""). Dans cet exemple, la base a t charge
correctement. Ca n'a pas toujours t le cas et c'est pourquoi nous avons ajout la mthode id pour avoir accs au message d'erreur.
Conclusion
248
L'erreur tait que le nom DSN de la base avait t dfinie comme DSN utilisateur alors qu'il fallait le dfinir comme DSN
systme. Cette distinction se fait dans le gestionnaire de sources ODBC 32 bits :
1
Conclusion
249
impots.cs
impots.dll
test.cs
test.exe
E:\data\serge\MSNET\c#\impots\6>test
pg DSNimpots tabImpots colLimites colCoeffR colCoeffN
E:\data\serge\MSNET\c#\impots\6>test mysql-impots timpots limites coeffr coeffn
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :o 2 200000
impt=22506 F
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :
Le programme test utilisait alors la classe impt classique celle contenue dans le fichier impots.dll. Le code du programme test.cs tait le
suivant :
using System;
class test
{
public static void Main(string[] arguments)
{
// programme interactif de calcul d'impt
// l'utilisateur tape trois donnes au clavier : mari nbEnfants salaire
// le programme affiche alors l'impt payer
const string syntaxe1="pg DSNimpots tabImpots colLimites colCoeffR colCoeffN";
const string syntaxe2="syntaxe : mari nbEnfants salaire\n"
+"mari : o pour mari, n pour non mari\n"
+"nbEnfants : nombre d'enfants\n"
+"salaire : salaire annuel en F";
// vrification des paramtres du programme
if(arguments.Length!=5){
Conclusion
250
// msg d'erreur
Console.Error.WriteLine(syntaxe1);
// fin
Environment.Exit(1);
}//if
// on rcupre les arguments
string DSNimpots=arguments[0];
string tabImpots=arguments[1];
string colLimites=arguments[2];
string colCoeffR=arguments[3];
string colCoeffN=arguments[4];
// cration d'un objet impt
impt objImpt=null;
try{
objImpt=new impt(DSNimpots,tabImpots,colLimites,colCoeffR,colCoeffN);
}catch (Exception ex){
Console.Error.WriteLine("L'erreur suivante s'est produite : " + ex.Message);
Environment.Exit(2);
}//try-catch
// boucle infinie
while(true){
// on demande les paramtres du calcul de l'impt
Console.Out.Write("Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour
arrter :");
string paramtres=Console.In.ReadLine().Trim();
// qq chose faire ?
if(paramtres==null || paramtres=="") break;
// vrification du nombre d'arguments dans la ligne saisie
string[] args=paramtres.Split(null);
int nbParamtres=args.Length;
if (nbParamtres!=3){
Console.Error.WriteLine(syntaxe2);
continue;
}//if
// vrification de la validit des paramtres
// mari
string mari=args[0].ToLower();
if (mari!="o" && mari !="n"){
Console.Error.WriteLine(syntaxe2+"\nArgument mari incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants=0;
try{
nbEnfants=int.Parse(args[1]);
if(nbEnfants<0) throw new Exception();
}catch (Exception){
Console.Error.WriteLine(syntaxe2+"\nArgument nbEnfants incorrect : tapez un entier positif ou
nul");
continue;
}//if
// salaire
int salaire=0;
try{
salaire=int.Parse(args[2]);
if(salaire<0) throw new Exception();
}catch (Exception){
Console.Error.WriteLine(syntaxe2+"\nArgument salaire incorrect : tapez un entier positif ou
nul");
continue;
}//if
// les paramtres sont corrects - on calcule l'impt
Console.Out.WriteLine("impt="+objImpt.calculer(mari=="o",nbEnfants,salaire)+" F");
// contribuable suivant
}//while
}//Main
}//classe
Nous reprenons le mme programme pour lui faire utiliser maintenant le service web impots au travers de la classe proxy impt cre
prcdemment. Nous sommes obligs de modifier quelque peu le code :
o alors que la classe impt d'origine avait un constructeur cinq arguments, la classe proxy impt a un constructeur sans
paramtres. Les cinq paramtres, nous l'avons vu, sont maintenant fixs dans le fichier de configuration du service web.
o il n'y a donc plus besoin de passer ces cinq paramtres en arguments au programme test
Le nouveau code est le suivant :
using System;
class test
{
public static void Main()
{
// programme interactif de calcul d'impt
// l'utilisateur tape trois donnes au clavier : mari nbEnfants salaire
// le programme affiche alors l'impt payer
const string syntaxe2="syntaxe : mari nbEnfants salaire\n"
Conclusion
251
...
// boucle infinie
while(true){
// on demande les paramtres du calcul de l'impt
}//while
}//Main
}//classe
17:20
17:53
5 120 impt.dll
2 324 test.cs
puis l'excutons :
E:\data\serge\MSNET\c#\impots\webservice>test
Paramtres du calcul de l'impt au format mari nbEnfants salaire ou rien pour arrter :o 2 200000
impt=22506 F
Conclusion
252
10. A suivre
Il resterait des thmes importants couvrir. En voici trois :
1.
2.
3.
les objets DataSet qui permettent de grer une base de donnes en mmoire, de l'exporter ou l'importer au format XML
une tude de XML avec les classes .NET permettant de grer les documents XML
la programmation Web avec les pages et contrles ASP.NET
A lui seul, le point 3 mrite un polycopi. Les points 1 et 2 devraient tre ajouts progressivement ce prsent document.
Conclusion
253