I. Introduction▲
Pour une introduction aux services web, je vous suggère de consulter cet article Introduction aux WebServices en .NET. L'application que nous vous proposons est une gestion de planning simple dont le principe est inspiré du calendrier d'Outlook. Nous allons parcourir ensemble sa création. Elle est composée de :
- une interface cliente ASP.NET/C# qui consomme le service web ;
- le service web lui-même qui interagit avec une base de données MsAccess ;
- un démon se chargeant d'envoyer les e-mails aux utilisateurs et de nettoyer la base de données.
Avant d'expliquer les concepts liés à l'application fournie, nous allons aborder les différentes possibilités des services web via des exemples indépendants de notre application. Étant donné qu'il existe plusieurs IDE permettant de développer en .NET (Visual Studio.NET, ASP.NET WebMatrix,SharpDevelop-Fidalgo), je vous montrerai comment utiliser des outils indépendants de ces IDE et dont le résultat fonctionnera, quel que soit l'IDE que vous utilisez pour vos projets.
I-A. Architecture des services web!▲
I-B. Élaboration d'un service web simple▲
Les services web sont des pages ASP.NET dont l'extension est asmx. À peu de choses près, ils ressemblent à des classes tout à fait classiques, seuls certains attributs les en différencient. Ils sont basés sur des protocoles de communication tels que XML et SOAP, tout ceci est véhiculé par les protocoles http et/ou https. Vous constaterez rapidement que grâce au framework .NET, vous n'aurez pas vraiment à vous soucier de XML et de SOAP, car le SDK fournit des outils générant les en-têtes SOAP et le XML nécessaire à votre place. L'usage de SOAP est toutefois optionnel. Son avantage consiste à pouvoir retourner des données de type complexe telles qu'un dataset par exemple. Sans SOAP, ce ne serait pas possible.
Voici un petit exemple classique tout simple permettant d'appréhender les services web
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Diagnostics;
using
System.
Web;
using
System.
Web.
Services;
namespace
PetitExemple
{
public
class
Exemple :
System.
Web.
Services.
WebService
{
[WebMethod]
/*Cet attribut spécifie que la méthode est utilisable par une application cliente d'un service Web*/
public
int
Additionne
(
int
a,
int
b)
{
return
a +
b;
}
}
}
L'attribut [WebMethod] permet de rendre une méthode accessible aux clients des services web. Cet attribut possède lui-même ses propres attributs optionnels que nous détaillerons dans la section 1.3. Vous pouvez bien sûr déclarer vos propres méthodes et propriétés comme dans une classe tout à fait classique. Si ces méthodes ne sont pas précédées de l'attribut [WebMethod], elles ne seront tout simplement pas accessibles aux consommateurs du service.
Vous pouvez ensuite tester directement votre service web en ouvrant votre navigateur et en tapant l'URL où il se situe. S’il se situe à la racine web, vous tapez l'URL « http://localhost/nomduservice.asmx » et vous obtiendrez ceci :
Notez que j'ai tronqué l'image, mais j'ai gardé la partie qui nous intéresse. Si vous cliquez sur le lien « additionne », vous obtiendrez ceci :
Cet écran vous permet de tester votre service web. En saisissant les valeurs des paramètres a et b et en cliquant sur « Invoke », vous obtiendrez la somme des paramètres comme ceci :
Cette somme est retournée sous format XML au client.
Chaque service web est identifié par un contrat wsdl. Ce contrat représente la structure SOAP/XML de votre service Web. Il est nécessaire de faire référence à ce contrat lorsque vous désirez consommer un service web. Le contrat WSDL est visible soit en cliquant sur le lien « Service description » soit en ajoutant « ?WSDL » à la fin de l'URL où se trouve votre service.
I-C. Consommation d'un service web simple▲
Pour consommer un service web, il faut connaître l'emplacement de celui-ci(dans notre exemple c'est un service web local)! Lorsque vous savez où il se trouve, vous devez obtenir son contrat WSDL qui vous permettra de créer une classe proxy permettant au consommateur de l'utiliser. Pour utiliser un service web que vous n'avez pas développé vous-même, vous pouvez utiliser le site UDDI.org qui répertorie et donne des informations sur la majorité des services web disponibles.
Selon l'IDE que vous utilisez, vous pouvez utiliser les fonctionnalités de celui-ci pour référencer votre service web. Ces fonctionnalités sont différentes pour chaque IDE. Je vais donc vous fournir une méthode générique permettant de générer la classe proxy utilisable par tous ces IDE.
Pour plus de confort, ajustez la variable d'environnement PATH et ajoutez-y le chemin du SDK où se trouvent les outils wsdl.exe et csc.exe. Ensuite, ouvrez une fenêtre DOS. Lorsque ceci est fait, vous devez simplement taper ceci pour générer la classe proxy comme illustré ci-dessous :
wsdl url_du_service_web?
WSDL /
out
:
lenomdelaclasseproxy.
cs
Cette commande va donc générer un fichier source que l'on appellera classe proxy. Cette classe sera l'interface entre vos fichiers sources et le service web. Vous n'aurez qu'à ajouter ce fichier source à votre projet où en faire une dll que vous ajouterez en tant que référence dans votre projet.
Pour créer cette DLL, vous pouvez utiliser l'utilitaire csc
csc /
target:
library petitExemple.
cs
En reprenant notre exemple de service « petitExemple » et en ayant procédé aux étapes de génération de la classe proxy, vous êtes désormais apte à consommer le service. Ajoutez le fichier source petitExemple.cs ou la dll petitExemple.dll à votre projet. Maintenant dans une page ASP.NET, vous pouvez désormais coder ceci :
<%
@ Page language=
"c#"
Codebehind=
"UtilisePetitExemple.aspx.cs"
AutoEventWireup=
"true"
Inherits=
"UtilisePetitExemple.ExempleConsommation"
%>
<HTML>
<body MS_POSITIONING
=
"GridLayout"
>
<form id
=
"Form1"
method
=
"post"
runat
=
"server"
>
<asp:Label id
=
"reponse_webservice"
style
=
"Z-INDEX: 101; LEFT: 232px; POSITION: absolute; TOP: 176px"
runat
=
"server"
Width
=
"264px"
>
Label</asp:Label>
</form>
</body>
</HTML>
Nous avons simplement construit une page ASP.NET contenant un contrôle label qui recevra la réponse du service web.
Voici à présent le code de la page UtilisePetitExemple.aspx.cs
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Drawing;
using
System.
Web;
using
System.
Web.
SessionState;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
namespace
UtilisePetitExemple
{
public
class
ExempleConsommation :
System.
Web.
UI.
Page
{
protected
System.
Web.
UI.
WebControls.
Label reponse_webservice;
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
/*On instancie le service web*/
Exemple petitExemple=
new
Exemple
(
);
/*On appelle la méthode "additionne" qui retournera "15"*/
reponse_webservice.
Text=
petitExemple.
Additionne
(
5
,
10
).
ToString
(
);
}
}
}
I-D. Les différents comportements possibles des services web▲
Par défaut, un service web n'a pas le même comportement entre lui-même et l'objet l'ayant instancié qu'une classe normale. En effet, le simple exemple suivant ne fonctionne pas avec un service web (nous reprenons l'exemple précédent quelque peu modifié)
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Diagnostics;
using
System.
Web;
using
System.
Web.
Services;
namespace
PetitExemple
{
public
class
Exemple :
System.
Web.
Services.
WebService
{
public
int
a;
/*On modifie l'exemple précédent en ajoutant cette méthode*/
[WebMethod]
public
void
stockeValeurCliente
(
int
valeurCliente)
{
a=
valeurCliente;
//On affecte au membre "a" la valeur "valeurCliente" reçue en paramètre
}
[WebMethod]
public
int
Additionne
(
int
b)
{
return
a +
b;
}
}
}
Lors de l'appel à ce service web lorsque vous ferez ceci (je ne retape pas tout le code ASP.NET, seulement le c#) :
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Drawing;
using
System.
Web;
using
System.
Web.
SessionState;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
namespace
UtilisePetitExemple
{
public
class
ExempleConsommation :
System.
Web.
UI.
Page
{
protected
System.
Web.
UI.
WebControls.
Label reponse_webservice;
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
Exemple petitExemple=
new
Exemple
(
);
/*Appel de la méthode supposée stocker la valeur 3*/
petitExemple.
stockeValeurCliente
(
3
);
/*L'appel de cette méthode retournera 10 et non 13*/
reponse_webservice.
Text=
petitExemple.
Additionne
(
10
).
ToString
(
);
}
}
}
Cet exemple démontre bien la différence de comportement entre une instance d'objet classique et les instances de services web. Dans un objet classique, la méthode « Additionne » aurait bel et bien retourné 13. Dans le cadre d'un service web et en l'occurrence, de l'exemple ci-dessus, lors de l'appel à la deuxième méthode, contrairement à un « dialogue » entre un objet classique et son instance, le membre « a » ayant été affecté lors de l'appel à la 1re méthode n'a désormais plus de valeur lors de l'appel à la deuxième méthode. Il n'est donc pas possible de travailler tel qu'avec une classe usuelle.
L'exemple ci-dessus n'est probablement pas le plus parlant, mais imaginez la même situation avec un service web nécessitant une authentification, si à chaque appel de méthode, le client doit envoyer les infos relatives à l'utilisateur (userid,pwd…), ça peut vite devenir laborieux.
Pour pallier cela, l'attribut WebMethod possède ses propres attributs dont l'un d'entre eux permet l'usage de session. En utilisant les sessions, le problème ci-dessus est contourné puisqu'on ne doit plus repasser à chaque appel de méthode, des paramètres génériques. L'attribut en question est « EnableSession ». Voyons en détail la liste de tous les attributs de [WebMethod]
[WebMethod (EnableSession=true)]
Par défaut, l'attribut EnableSession est à false, ce qui signifie que vous ne pouvez stocker des données en session et que de ce fait, aucune donnée ne peut être persistante entre le consommateur et le service. Pour activer cette persistance, il suffit de définir cet attribut à true dans l'attribut [WebMethod]. Sachez toutefois que permettre l'usage de sessions entre le consommateur et le service Web peut avoir un impact non négligeable sur les performances. De plus, le consommateur doit accepter les cookies, car un cookie de session persistant permettant au service d'identifier ce client doit être créé. L'application que nous fournissons vous montrera comment cela fonctionne un peu plus loin dans ce tuto.
[WebMethod (BufferResponse=false)]
Par défaut, cet attribut est défini à true. Lorsqu'il est à true, le service web crée une mémoire tampon stockant sa réponse et lorsque l'entièreté de ce contenu est dans le tampon, il envoie la réponse au consommateur. Lorsque l'on définit cet attribut à false, le service web envoie sa réponse au fur et à mesure qu'il la sérialise. Là encore, en changeant la valeur par défaut, un impact non négligeable sur les performances est à craindre. De manière générale, on ne modifiera cet attribut que si l'on sait pertinemment que la réponse du service web atteint un poids considérable de donnée et qu'elle risque de saturer la mémoire. Il faut aussi noter que lorsque cet attribut est défini à false, les en-têtes SOAP sont désactivés et de ce fait, certains objets complexes (Dataset par ex.) ne peuvent plus être véhiculés vers le consommateur/service.
[WebMethod (CacheDuration=nombre de secondes)]
Par défaut, aucune rétention de données en cache n'est effectuée. Vous pouvez changer ce comportement en disant au service web de garder les réponses en cache pendant un certain temps. L'avantage d'utiliser cette technique est évident puisqu'en cas de demande/réponse déjà rencontrée, les performances seront bien meilleures. Le désavantage est aussi évident, car bien sûr, ces données seront stockées en mémoire cache et encombreront donc la mémoire. Je pense qu'il peut-être nécessaire d'utiliser cet attribut avec une méthode de service très sollicitée. Dans tous les cas, il faut l'utiliser avec précaution pour éviter des situations où ce système deviendrait contre-performant. Pour faire un court parallèle avec les bases de données, tous les DBA savent qu'il faut éviter d'utiliser des index à tort et à travers et qu'il ne faudrait les utiliser que pour des tables étant requêtées (SELECT) au moins à 75 %. En aucun cas il ne faudrait utiliser des index pour des tables étant sans cesse mise à jour, car dans ce cas, l'index devrait chaque fois être mis à jour lui-même et serait donc contre-performant.
[WebMethod (Description=« une description »)]
Je pense qu'il n'est pas nécessaire d'expliquer l'utilité de cet attribut.
[WebMethod (MessageName=« alias nom de méthode »)]
Cet attribut permet la surcharge de méthodes du service. C'est la valeur de cet attribut qui est utilisé lors de la communication entre le consommateur et le service. Il doit identifier votre méthode web de manière unique. S’il n'est pas défini, c'est le nom de la méthode elle-même qui sera utilisé.
[WebMethod (TransactionOption=TransactionOption.[Disabled ou Required ou Supported ou NotSupported ou RequiresNew])]
Cet attribut contient lui-même plusieurs sous attributs mis entre crochets :
- Disabled : les transactions sont désactivées ;
- Required : les transactions sont obligatoires, l'appel à la méthode du service doit être au sein d'une transaction ;
- Supported : les transactions sont supportées ;
- NotSupported : les transactions ne sont pas supportées ;
- RequiresNew : chaque méthode doit démarrer une transaction.
I-E. Les appels asynchrones▲
Par défaut, lorsque l'on appelle une méthode d'un service web, l'appel est dit synchrone, c'est-à -dire que l'appelant attend que l'exécution de la méthode appelée soit terminée pour reprendre la main. Dans le cadre d'un service web, cela peut-être ennuyeux, car étant donné que le service est souvent distant, l'exécution d'une méthode et le retour de la réponse peut nécessiter un temps non négligeable. Dans pareille situation, le processus appelant reste bloqué et l'utilisateur peut s'irriter.
Il y a toutefois une parade à cela, c'est l'utilisation d'appels asynchrones. Ceux-ci sont par définition opposés aux appels synchrones. Ils n'attendent donc pas que la méthode appelée ait terminé son exécution pour continuer leur propre traitement. L'utilisation d'appels asynchrones n'est nécessaire que si l'on désire effectuer un traitement particulier pendant l'attente de la réponse du service web. Si notre traitement dépend exclusivement de la réponse du service web, il est alors bien sûr inutile de les utiliser.
Lorsque vous générez une classe proxy pour un service web donné, l'outil wsdl génère le code nécessaire pour travailler avec les méthodes synchrones et asynchrones. Les méthodes synchrones portent le même nom que les méthodes du service Web. Les méthodes asynchrones sont par contre préfixées de cette manière :
- Begin<nom de la méthode du service> démarre la communication asynchrone ;
- End<nom de la méthode du service> reçoit la réponse de la méthode en fin de communication.
Reprenons toujours le même exemple, encore une fois quelque peu modifié. Le service web restant pour sa part inchangé
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Drawing;
using
System.
Web;
using
System.
Web.
SessionState;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
namespace
UtilisePetitExemple
{
public
class
ExempleConsommation :
System.
Web.
UI.
Page
{
protected
System.
Web.
UI.
WebControls.
Label reponse_webservice;
private
Exemple petitExemple=
new
Exemple
(
);
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
//Déclaration d'un objet résultat d'appel asynchrone et démarrage asynchrone de l'appel*/
IAsyncResult Reponse =
petitExemple.
BeginAdditionne
(
10
,
15
,
null
,
null
);
Response.
Write
(
"Le traitement continue"
);
/**************************************************
* Ici vous pouvez exécuter n'importe quel code *
* ************************************************/
/*Ici vous bloquez l'exécution du code jusqu'à ce que
* la réponse de l'appel asynchrone a été obtenue */
Reponse.
AsyncWaitHandle.
WaitOne
(
);
/*Ici vous récupérez la réponse --> 25*/
reponse_webservice.
Text=
petitExemple.
EndAdditionne
(
Reponse).
ToString
(
);
}
}
}
Cet exemple est très simpliste, car un seul appel asynchrone est réalisé. Cette gestion d'appels asynchrones peut s'avérer bien plus complexe lorsque plusieurs appels à la même méthode sont effectués. En règle générale, il faut donc retenir que l'outil WSDL génère les méthodes permettant de travailler en mode asynchrone et qu'il y a deux grands types d'appels asynchrones. L'exemple ci-dessus illustre la première méthode et l'exemple ci-dessous la deuxième.
Appel asynchrone implémentant une méthode de callback aussi appelée méthode de finalisation.
using
System;
using
System.
ComponentModel;
using
System.
Web;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
namespace
UtilisePetitExemple
{
public
class
ExempleConsommation :
System.
Web.
UI.
Page
{
protected
System.
Web.
UI.
WebControls.
Label reponse_webservice;
private
Exemple petitExemple=
new
Exemple
(
);
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
/*cb permet d'assigner une fonction de callback, en l'occurrence "AdditionneCallback"*/
AsyncCallback cb =
new
AsyncCallback
(
AdditionneCallback);
/*Démarrage de l'appel asynchrone*/
IAsyncResult Reponse=
petitExemple.
BeginAdditionne
(
10
,
15
,
cb,
petitExemple);
/*Ajoutez le code qu'il vous plaira*/
Response.
Write
(
"Le traitement continue"
);
/**********************************************************
* La fonction de callback sera automatiquement appelée *
* Lorsque la réponse de l'appel asynchrone sera complète *
* et que vous aurez requis cette réponse via waitone *
**********************************************************/
Reponse.
AsyncWaitHandle.
WaitOne
(
);
}
/*Cette méthode est la méthode de callback*/
public
void
AdditionneCallback
(
IAsyncResult Reponse)
{
Exemple petitExemple=(
Exemple)Reponse.
AsyncState;
int
RetourStr=
petitExemple.
EndAdditionne
(
Reponse);
reponse_webservice.
Text=
RetourStr.
ToString
(
);
}
}
}
Enfin, pour en terminer avec les appels asynchrones, voyons comment gérer plusieurs appels asynchrones. Bien d'autres choses devraient encore être expliquées concernant le mode asynchrone, mais tel n'est pas le but de ce tutoriel. Il faudrait un tutoriel complet sur ce mode.
Appels multiples :
using
System;
using
System.
ComponentModel;
using
System.
Web;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
using
System.
Threading;
namespace
UtilisePetitExemple
{
public
class
ExempleConsommation :
System.
Web.
UI.
Page
{
private
Exemple petitExemple=
new
Exemple
(
);
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
IAsyncResult appel1 =
petitExemple.
BeginAdditionne
(
10
,
15
,
null
,
null
);
IAsyncResult appel2 =
petitExemple.
BeginAdditionne
(
15
,
20
,
null
,
null
);
Response.
Write
(
"Le traitement continue"
);
/**********************************************************
* Ici on bloque le code jusqu'Ã ce que tous les appels *
* asynchrones soient terminés *
**********************************************************/
WaitHandle[]
appels =
{
appel1.
AsyncWaitHandle,
appel2.
AsyncWaitHandle};
/*Les deux appels suivants afficheront respectivement 25 et 35*/
Response.
Write
(
petitExemple.
EndAdditionne
(
appel1));
Response.
Write
(
petitExemple.
EndAdditionne
(
appel2));
}
}
}
II. Notre petite application▲
Pour rappel, cette application est un gestionnaire de tâches ressemblant un peu au calendrier d'Outlook. Elle se compose d'un service web utilisant les sessions, d'un consommateur et d'un démon se chargeant d'envoyer les e-mails aux utilisateurs désireux d'être averti lorsqu'une ou plusieurs de leurs tâches arrivent à échéance.
II-A. Le service web▲
/*
Remarque générale: les exceptions pouvant survenir ont été trappée de manière à indiquer
au consommateur du service si la méthode appelée a réussi ou échoué. Les messages d'exceptions
n'ont donc pas été traités. Globalement les méthodes retournent true ou false selon qu'elles
ont échoué ou non.
*/
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Data.
OleDb;
using
System.
Diagnostics;
using
System.
Web;
using
System.
Web.
Services;
using
System.
Text.
RegularExpressions;
namespace
AgendaWS
{
public
class
dvpPlanning :
System.
Web.
Services.
WebService
{
//La classe hérite de la classe webservice
private
string
strConn;
//chaine de connexion
//Cette méthode est privée et sert à établir la connexion à la DB
private
bool
initDb
(
)
{
strConn=
"Provider=Microsoft.Jet.OLEDB.4.0;Data source="
+
Server.
MapPath
(
"db/planning.mdb"
);
conn=
new
OleDbConnection
(
strConn);
//Connexion à la db
try
{
this
.
conn.
Open
(
);
//Ouverture de la db
}
catch
{
return
false
;
}
return
true
;
}
/*Méthode d'authentification + ajout d'un nouvel utilisateur***********************************
* Cette authentification devrait normalement utiliser un système d'encryption afin *
* de ne pas stocker en clair les mots de passe dans la DB. De plus, pour une sécurité totale *
* tout ceci devrait transiter en HTTPS plus éventuellement un module d'encryption au niveau *
* du serveur web. Ce n'est pas le sujet de ce tutoriel, je me suis donc contenté d'une *
* authentification très basique*
***********************************************************************************************
[WebMethod (Description="Authentifie l'utilisateur",EnableSession=true)]
public bool getAuth(string u,string p,bool newUser,string Lname,string Fname,string Email)
{
if(newUser)
{
/*On vérifie qu'en cas d'ajout d'utilisateur, tous les paramètres requis sont fournis*/
if
(
Lname==
null
||
Fname==
null
||
Email ==
null
){
return
false
;}
/*Vérification du format de l'adresse Email*/
Regex CheckEmail =
new
Regex
(
"^[a-z]{1,}@[a-z]{1,}
\\
.[a-z]{2,3}$"
);
if
(!
CheckEmail.
IsMatch
(
e-
mail))
{
return
false
;
}
}
if
(!
initDb
(
)){
return
false
;}
string
strSql=
"select count(*) as auth from Users where UserId='"
+
u+
"' and Pwd='"
+
p+
"'"
;
OleDbCommand cmd=
new
OleDbCommand
(
strSql,
conn);
OleDbDataReader cmdReader=
cmd.
ExecuteReader
(
);
cmdReader.
Read
(
);
int
auth=
cmdReader.
GetInt32
(
0
);
cmdReader.
Close
(
);
if
(
auth ==
1
)
{
if
(!
newUser)/*Si c'est un user existant*/
{
Session[
"User"
]=
u;
return
true
;
}
else
/*Sinon il existe déjà on ne peut pas l'ajouter*/
{
return
false
;
}
}
else
{
if
(!
newUser)
{
return
false
;
}
else
{
strSql=
"insert into Users(UserId,LastName,FirstName,Pwd,Email) "
;
strSql+=
"values('"
+
u+
"','"
+
Lname+
"','"
+
Fname+
"','"
+
p+
"','"
+
e-
mail+
"')"
;
try
{
cmd=
new
OleDbCommand
(
strSql,
conn);
cmd.
ExecuteNonQuery
(
);
}
catch
{
return
false
;
}
}
}
return
true
;
}
/*Méthode retournant les tâches du consommateur selon le jour choisi
* par celui-ci*/
[WebMethod (Description=
"Retourne un DataSet avec les tâches"
,EnableSession=true)]
public
DataSet getMyPlanning
(
DateTime dt)
{
if
(
Session[
"User"
]==
null
){
return
null
;}
DateTime dtNext =
dt;
dtNext=
dt.
AddDays
(
1
);
DataSet data=
new
DataSet
(
);
if
(!
initDb
(
)){
return
data;}
string
strSql=
"select IdPlanning,Format(PlanStartRange,'Short Time') as PlanStartRange,"
;
strSql+=
"Format(PlanEndRange,'Short Time') as PlanEndRange,Task,remind from planning "
;
strSql+=
"where userid='"
+
Session[
"User"
]+
"' and PlanStartRange>=#"
+
dt+
"# and PlanStartRange"
;
strSql+=
"<#"
+
dtNext+
"#"
;
OleDbDataAdapter planData=
new
OleDbDataAdapter
(
strSql,
strConn);
planData.
Fill
(
data,
"planning"
);
return
data;
}
[WebMethod (Description=
"Retourne les heures valides"
,EnableSession=true)]
public
ArrayList getHourList
(
)
{
ArrayList heures =
new
ArrayList
(
);
int
i;
if
(
Session[
"User"
]!=
null
)
{
for
(
i=
8
;
i<
24
;
i++
)
{
heures.
Add (
i+
":00-"
+
i+
":30"
);
}
}
return
heures;
}
[WebMethod (Description=
"Met à jour une tâche de la DB"
,EnableSession=true)]
public
bool
updatePlanning
(
int
rowId,
string
task,
bool
remind)
{
if
(
Session[
"User"
]!=
null
)
{
if
(!
initDb
(
)){
return
false
;}
string
strDML=
"update planning set Task='"
+
task+
"',remind="
+
remind;
strDML+=
" where IdPlanning="
+
rowId;
OleDbCommand cmd=
new
OleDbCommand
(
strDML,
conn);
try
{
cmd.
ExecuteNonQuery
(
);
}
catch
{
return
false
;
}
}
else
{
return
false
;
}
return
true
;
}
[WebMethod (Description=
"Ajoute une tâche dans la DB"
,EnableSession=true)]
public
bool
addToPlanning
(
string
hour,
DateTime day,
string
Task,
bool
remind)
{
if
(
Session[
"User"
]==
null
){
return
false
;}
/*On vérifie que les paramètres sont non nuls*/
if
(
hour ==
""
||
day.
ToString
(
)==
""
||
Task ==
""
){
return
false
;}
/* ici commence la transformation de la chaîne heure reçue sous la forme
* hh:mm - hh:mm */
string
hourSplit=
hour;
string
delimStr =
"-"
;
char
[]
delimiter=
delimStr.
ToCharArray
(
);
string
[]
hourSplitted=
hourSplit.
Split
(
delimiter,
2
);
delimStr=
":"
;
delimiter=
delimStr.
ToCharArray
(
);
string
[]
hourStart =
hourSplitted[
0
].
Split
(
delimiter,
2
);
string
[]
minute=
hourSplitted[
1
].
Split
(
delimiter,
2
);
/*Ici on converti l'heure reçue en int32 et on retourne false
* si la conversion ne réussit pas (en cas de paramètre invalide par exemple)
* si le client bidouille le format*/
int
hourToAdd;
try
{
hourToAdd=
Convert.
ToInt32
(
hourStart[
0
]
);
}
catch
{
return
false
;
}
/*Ici on converti les minutes reçues en int32 et on retourne false
* si la conversion ne réussit pas (en cas de paramètre invalide par exemple)
* si le client bidouille le format*/
int
minuteToAdd;
try
{
minuteToAdd=
Convert.
ToInt32
(
minute[
1
]
);
}
catch
{
return
false
;
}
/*On vérifie que les minutes sont bien égales à 30, seule valeur autorisée*/
if
(
minuteToAdd !=
30
)
{
return
false
;
}
/*Ici on convertit le jour choisi au niveau du calendrier et on y
* ajoute les heures et minutes reçues pour définir la date de début
* et la date de fin pour cette tâche*/
DateTime startTime;
try
{
startTime=((
DateTime)day).
AddHours
(
hourToAdd);
}
catch
{
return
false
;
}
DateTime endTime;
try
{
endTime=
startTime.
AddMinutes
(
minuteToAdd);
}
catch
{
return
false
;
}
if
(!
initDb
(
)){
return
false
;}
string
strSql=
"insert into planning(UserId,PlanStartRange,"
;
strSql+=
"PlanEndRange,Task,Remind) values("
+
Session[
"User"
]+
",#"
;
strSql+=
startTime+
"#,#"
+
endTime+
"#,'"
+
Task+
"',"
+
remind+
")"
;
OleDbCommand cmd=
new
OleDbCommand
(
strSql,
conn);
try
{
cmd.
ExecuteNonQuery
(
);
}
catch
{
return
false
;
}
return
true
;
}
[WebMethod (Description=
"Supprime une tâche de la DB"
,EnableSession=true)]
public
string
deleteFromPlanning
(
int
rowId)
{
if
(
Session[
"User"
]!=
null
)
{
if
(!
initDb
(
)){
return
"Connexion DB impossible"
;}
OleDbCommand cmd=
new
OleDbCommand
(
"delete from planning where IdPlanning="
+
rowId,
conn);
try
{
cmd.
ExecuteNonQuery
(
);
}
catch
(
Exception E)
{
return
E.
GetBaseException
(
).
ToString
(
);
}
}
else
{
return
"Non authentifié!"
;
}
return
"Mise à jour effectuée"
;
}
}
}
II-B. L'interface cliente (consommatrice du service web)▲
La page de login permet soit de se connecter, soit de s'enregistrer en tant que nouvel utilisateur. L'interface cliente est en tout composée de deux pages. La page de login et la page affichant le planning. Une simple vérification des variables de session est effectuée afin de vérifier si l'utilisateur est correctement authentifié ou non. Dans le cas où l'utilisateur n'est pas reconnu, une redirection vers la page de login est exécutée.
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Drawing;
using
System.
Web;
using
System.
Web.
SessionState;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
namespace
Agenda
{
public
class
Login :
System.
Web.
UI.
Page
{
protected
System.
Web.
UI.
WebControls.
Button Button1;
protected
System.
Web.
UI.
WebControls.
Label Label2;
protected
System.
Web.
UI.
WebControls.
Label Label1;
protected
System.
Web.
UI.
WebControls.
Label Label3;
protected
System.
Web.
UI.
WebControls.
Label Label4;
protected
System.
Web.
UI.
WebControls.
Label Label5;
protected
System.
Web.
UI.
WebControls.
TextBox Email;
protected
System.
Web.
UI.
WebControls.
TextBox LstName;
protected
System.
Web.
UI.
WebControls.
TextBox FrstName;
protected
System.
Web.
UI.
WebControls.
TextBox UserID;
protected
System.
Web.
UI.
WebControls.
Button NewUsr;
protected
System.
Web.
UI.
WebControls.
Label ErrorMsg;
protected
System.
Web.
UI.
WebControls.
TextBox Pwd;
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
if
(!
IsPostBack)
{
Session[
"NewUser"
]=
false
;
}
InitializeComponent
(
);
if
(
UserID.
Text!=
""
&&
Pwd.
Text!=
""
)
{
Session[
"UserID"
]=
UserID.
Text;
Session[
"Pwd"
]=
Pwd.
Text;
if
((
bool
)Session[
"NewUser"
]
)
{
if
(
FrstName.
Text==
""
||
LstName.
Text==
""
||
Email.
Text==
""
)
{
ErrorMsg.
Text=
"Tous les champs doivent être remplis"
;
}
else
{
Session[
"FrstName"
]=
FrstName.
Text;
Session[
"LstName"
]=
LstName.
Text;
Session[
"e-mail"
]=
Email.
Text;
}
}
Response.
Redirect
(
"agenda.aspx"
);
}
}
private
void
InitializeComponent
(
)
{
this
.
NewUsr.
Click +=
new
System.
EventHandler
(
this
.
NewUsr_Click);
}
private
void
NewUsr_Click
(
object
sender,
System.
EventArgs e)
{
if
(
NewUsr.
Text==
"Je désire m'enregistrer"
)
{
LstName.
Visible=
true
;
FrstName.
Visible=
true
;
Email.
Visible=
true
;
Label3.
Visible=
true
;
Label4.
Visible=
true
;
Label5.
Visible=
true
;
NewUsr.
Text=
"Je désire simplement me connecter"
;
Session[
"NewUser"
]=
true
;
}
else
{
LstName.
Visible=
false
;
FrstName.
Visible=
false
;
Email.
Visible=
false
;
Label3.
Visible=
false
;
Label4.
Visible=
false
;
Label5.
Visible=
false
;
NewUsr.
Text=
"Je désire m'enregistrer"
;
Session[
"NewUser"
]=
false
;
}
}
}
}
La page du planning :
using
System;
using
System.
Collections;
using
System.
ComponentModel;
using
System.
Data;
using
System.
Drawing;
using
System.
Web;
using
System.
Web.
SessionState;
using
System.
Web.
UI;
using
System.
Web.
UI.
WebControls;
using
System.
Web.
UI.
HtmlControls;
using
System.
Net;
namespace
Agenda
{
public
class
AgendaUtil :
System.
Web.
UI.
Page
{
protected
System.
Web.
UI.
WebControls.
DataGrid dg;
protected
System.
Web.
UI.
WebControls.
Calendar choixDate;
private
dvpPlanning ws=
new
dvpPlanning
(
);
//On instancie le service web
protected
System.
Web.
UI.
WebControls.
Label info;
protected
System.
Web.
UI.
WebControls.
DropDownList Heure;
protected
System.
Web.
UI.
WebControls.
CheckBox SendMail;
protected
System.
Web.
UI.
WebControls.
TextBox Tache;
protected
System.
Web.
UI.
WebControls.
Label info2;
protected
System.
Web.
UI.
WebControls.
Button Inserer;
private
void
Page_Load
(
object
sender,
System.
EventArgs e)
{
if
(
Session[
"UserID"
]==
null
||
Session[
"Pwd"
]==
null
) Response.
Redirect
(
"Login.aspx"
);
InitializeComponent
(
);
//C'est ici que l'on crée un cookie persistant entre le service et le consommateur
//afin de pouvoir travailler avec une session.
CookieContainer checkCookie;
if
(
Session[
"checkCookie"
]
==
null
)
checkCookie=
new
CookieContainer
(
);
else
checkCookie =
(
CookieContainer) Session[
"checkCookie"
];
ws.
CookieContainer =
checkCookie;
//Ici on vérifie s'il s'agit d'une authentification ou de l'enregistrement
//d'un nouvel utilisateur
if
((
bool
)Session[
"NewUser"
]
)
{
Response.
Write
(
"mail"
+
Session[
"e-mail"
].
ToString
(
));
Response.
Write
(
"frst"
+
Session[
"FrstName"
].
ToString
(
));
Response.
Write
(
"lst"
+
Session[
"LstName"
].
ToString
(
));
Response.
Write
(
Session[
"UserID"
].
ToString
(
));
Response.
Write
(
Session[
"Pwd"
].
ToString
(
));
if
(!
ws.
getAuth
(
Session[
"UserID"
].
ToString
(
),
Session[
"Pwd"
].
ToString
(
),
true
,
Session"LstName"
].
ToString
(
),
Session[
"FrstName"
].
ToString
(
),
Session[
"e-mail"
].
ToString
(
)))
{
Response.
Redirect
(
"login.aspx"
);
}
}
else
{
if
(!
ws.
getAuth
(
Session[
"UserID"
].
ToString
(
),
Session[
"Pwd"
].
ToString
(
),
false
,
null
,
null
,
null
))
{
Response.
Redirect
(
"login.aspx"
);
}
}
if
(
!
IsPostBack)
{
Session[
"dateSelected"
]=
DateTime.
Today;
//On appelle la méthode getHourList du service web afin de remplir la liste
//avec les heures valides
Heure.
DataSource =
ws.
getHourList
(
);
Heure.
DataBind
(
);
BindGrid
(
);
}
}
//DgEdit,DgCancel,DgUpdate,DgDelete,ItemsGrid_Command et BindGrid sont
//toutes des méthodes liées au datagrid et gère les évènements de celui-ci
public
void
DgEdit
(
Object sender,
DataGridCommandEventArgs E)
{
dg.
EditItemIndex =
(
int
)E.
Item.
ItemIndex ;
BindGrid
(
);
}
public
void
DgCancel
(
Object sender,
DataGridCommandEventArgs E)
{
dg.
EditItemIndex =
-
1
;
BindGrid
(
) ;
}
public
void
ItemsGrid_Command
(
Object sender,
DataGridCommandEventArgs e)
{
switch
(((
LinkButton)e.
CommandSource).
CommandName)
{
case
"Supprimer"
:
DgDelete
(
e);
break
;
default
:
break
;
}
}
public
void
DgUpdate
(
Object sender,
DataGridCommandEventArgs e)
{
TextBox updatedTask=(
TextBox)e.
Item.
Cells[
2
].
Controls[
0
];
int
rowId=(
int
)dg.
DataKeys[
e.
Item.
ItemIndex];
CheckBox Reminder=(
CheckBox)e.
Item.
FindControl
(
"RemindMe"
);
if
(
ws.
updatePlanning
(
rowId,
updatedTask.
Text,
Reminder.
Checked))
{
info.
Text=
"Mise à jour effectuée!"
;
}
else
{
info.
Text=
"Mise à jour non effectuée!"
;
}
dg.
EditItemIndex =
-
1
;
BindGrid
(
);
}
//Méthode appelée lorsque l'utilisateur supprime un enreg.
//du datagrid
public
void
DgDelete
(
DataGridCommandEventArgs E)
{
info.
Text=
ws.
deleteFromPlanning
((
int
)dg.
DataKeys[(
int
)E.
Item.
ItemIndex]
);
BindGrid
(
);
}
//Méthode utilisée pour la pagination
public
void
dg_PageIndexChanged
(
object
sender,
DataGridPageChangedEventArgs E)
{
dg.
CurrentPageIndex=
E.
NewPageIndex;
BindGrid
(
);
}
//Méthode liant les données aux datagrid. Elle appelle la méthode
//getMyPlanning du service web
private
void
BindGrid
(
)
{
DataSet ds=
new
DataSet
(
);
ds=
ws.
getMyPlanning
((
DateTime)Session[
"dateSelected"
]
);
if
(
ds!=
null
)
{
dg.
DataSource=
ds;
dg.
DataBind
(
);
if
(
ds.
Tables[
0
].
Rows.
Count ==
0
)
{
info2.
Text=
"Aucune tâche pour ce jour!"
;
}
else
{
info2.
Text=
"Liste de vos tâches("
+
ds.
Tables[
0
].
Rows.
Count+
")"
;
}
}
}
private
void
InitializeComponent
(
)
{
this
.
choixDate.
SelectionChanged +=
new
System.
EventHandler
(
this
.
choixDate_SelectionChanged);
this
.
Inserer.
Click +=
new
System.
EventHandler
(
this
.
Inserer_Click);
}
private
void
choixDate_SelectionChanged
(
object
sender,
System.
EventArgs e)
{
Session[
"dateSelected"
]=
choixDate.
SelectedDate;
BindGrid
(
);
}
private
void
Inserer_Click
(
object
sender,
System.
EventArgs e)
{
if
(
Heure.
SelectedItem.
Text!=
""
&&
Tache.
Text!=
""
)
{
//Ici on ajoute une tâche pour l'utilisateur courant
if
(
ws.
addToPlanning
(
Heure.
SelectedItem.
Text,(
DateTime)Session[
"DateSelected"
],
Tache.
Text,
SendMail.
Checked))
{
info.
Text=
"Mise à jour effectuée"
;
}
else
{
info.
Text=
"Mise à jour non effectuée"
;
}
BindGrid
(
);
}
}
}
}
II-C. Le démon▲
Le démon se charge d'expédier les e-mails aux utilisateurs ayant demandé à être avertis lorsque l'une de leurs tâches arrive à échéance. Toutes les 59 secondes, il interroge la base de données Access et détermine s’il doit envoyer des e-mails ou non. Outre la gestion des avertissements, il nettoie la DB et supprime toutes les tâches du mois précédent, ceci afin de garder une DB pas trop volumineuse. Les paramètres SMTP et l'emplacement du fichier .mdb d'Access sont déterminés dans le fichier de configuration « param.ini » qui doit se trouver dans le même répertoire que l'exécutable. Ceci permet donc une certaine souplesse, car la configuration est dynamique. La DB devrait normalement se trouver dans le répertoire DB du répertoire virtuel où se situe le service web.
Toutes les exceptions gérées pouvant survenir sont écrites dans un fichier .log. et provoquent l'arrêt du programme. En cas d'exception, le démon créera dynamiquement un répertoire log s'il n'existe déjà et ajoutera les messages d'erreurs dans le fichier « monitor.log ».
/* * Ce script est un daemon qui requête la base Access toutes les
* 59 secondes afin de déterminer si certaines tâches ont atteint
* leur échéance dans le temps. Si tel est le cas et que l'utilisateur
* concerné a demandé à être averti, ce script lui enverra un mail
* lui rappelant la tâche en question.
*/
using
System;
using
System.
IO;
using
System.
Threading;
using
System.
Web.
Mail;
using
System.
Data.
OleDb;
using
Ayende;
//Namespace trouvable ici http://dotnet.developpez.com/sources/csharp/?page=Files
class
checkAccess
{
private
string
dbF;
//Chemin du fichier MDB (fic. ini)
private
string
fMail;
//Mail de l'expéditeur (fic. ini)
private
string
sSMTP;
//Serveur SMTP (fic. ini)
static
TimeSpan waitTime =
new
TimeSpan
(
0
,
0
,
59
);
//Temporisation
public
checkAccess
(
) //Constructeur
{
getInitParams
(
);
//Réception des paramètres du fichier ini
Thread checkMail=
null
;
//Toutes les 59 sec. on crée un nouveau thread qui requêtera la db
try
{
checkMail =
new
Thread
(
new
ThreadStart
(
dbCheck));
}
catch
(
Exception e)
{
logIt
(
e.
Message);
Environment.
Exit
(
0
);
}
Console.
WriteLine
(
DateTime.
Now+
": Démarrage du thread "
+
checkMail.
GetHashCode
(
));
checkMail.
Start
(
);
//Boucle infinie, CTRL+C pour tuer le programme
while
(
true
)
{
if
(
checkMail.
Join
(
waitTime))
{
checkMail =
new
Thread
(
new
ThreadStart
(
dbCheck));
Console.
WriteLine
(
DateTime.
Now+
": Démarrage du thread "
+
checkMail.
GetHashCode
(
));
checkMail.
Start
(
);
}
}
}
//Ici on récupère les variables de configuration définies dans le fichier param.ini
private
void
getInitParams
(
)
{
using
(
TextReader streamReader =
new
StreamReader
(
"param.ini"
))
{
Configuration Params=
new
Configuration
(
streamReader);
dbF=
Params.
GetValue
(
"dbFile"
);
sSMTP=
Params.
GetValue
(
"serveur SMTP"
);
fMail=
Params.
GetValue
(
"fromMail"
);
}
}
private
void
dbCheck
(
)
{
Console.
WriteLine
(
DateTime.
Now+
": Thread initialisé"
);
OleDbConnection conn=
new
OleDbConnection
(
"Provider=Microsoft.Jet.OLEDB.4.0;Data source="
+
dbF);
try
{
conn.
Open
(
);
}
catch
(
Exception e)
{
logIt
(
e.
Message);
Environment.
Exit
(
1
);
}
DateTime dt2hours=
DateTime.
Now.
AddHours
(
2
);
//Cette requête récupère tous les enregistrements pour lesquels les utilisateurs
//ont demandé à être averti et dont l'heure de début de tâche se situe entre 2h
//avant l'heure courante et l'heure courante
string
sql=
"select task,lastName,firstname,e-mail,planning.userid"
;
sql+=
",idplanning from Users inner join planning on users.userid"
;
sql+=
"=planning.userid where planstartrange between #"
+
DateTime.
Now.
Month;
sql+=
"/"
+
DateTime.
Now.
Day+
"/"
+
DateTime.
Now.
Year+
" "
+
DateTime.
Now.
Hour;
sql+=
":"
+
DateTime.
Now.
Minute+
":"
+
DateTime.
Now.
Second+
"# and "
;
sql+=
"#"
+
dt2hours.
Month+
"/"
+
dt2hours.
Day+
"/"
+
dt2hours.
Year+
" "
+
dt2hours.
Hour;
sql+=
":"
+
dt2hours.
Minute+
":"
+
dt2hours.
Second+
"# "
;
sql+=
"and reminded=false and remind=true order by planning.userid"
;
OleDbCommand cmd=
null
;
OleDbDataReader cmdReader=
null
;
try
{
cmd=
new
OleDbCommand
(
sql,
conn);
cmdReader=
cmd.
ExecuteReader
(
);
}
catch
(
Exception e)
{
logIt
(
e.
Message);
Environment.
Exit
(
0
);
}
string
prevUser=
""
;
string
tBody=
null
;
string
tTitle=
null
;
string
adr=
null
;
string
idPlanning=
null
;
//On effectue une rupture afin de regrouper toutes les tâches
//d'un utilisateur dans un seul e-mail.
while
(
cmdReader.
Read
(
))
{
if
(
prevUser.
ToString
(
)!=
cmdReader.
GetString
(
4
) &&
prevUser!=
""
)
{
sendMsg
(
adr,
tTitle+
tBody);
logIt
(
DateTime.
Now+
":"
+
adr);
tTitle=
tBody=
adr=
null
;
}
if
(
tTitle==
null
)
{
tTitle=
"Cher "
+
cmdReader.
GetString
(
1
)+
" "
+
cmdReader.
GetString
(
2
)+
",
\n
"
;
tTitle+=
"Voici la liste des tâches pour lesquelles vous avez souhaité "
;
tTitle+=
"obtenir un rappel:
\n\n
"
;
adr=
cmdReader.
GetString
(
3
);
}
tBody+=
cmdReader.
GetString
(
0
);
prevUser=
cmdReader.
GetString
(
4
);
idPlanning+=
cmdReader.
GetInt32
(
5
)+
","
;
}
cmdReader.
Close
(
);
if
(
adr!=
null
)
{
sendMsg
(
adr,
tTitle+
tBody);
logIt
(
DateTime.
Now+
":"
+
adr);
cmd.
CommandText=
"update planning set reminded=true where IdPlanning in ( "
;
cmd.
CommandText+=
idPlanning.
Substring
(
0
,
idPlanning.
Length-
1
)+
")"
;
cmd.
ExecuteNonQuery
(
);
Console.
WriteLine
(
"Update DB effectué"
);
}
/*Démarrage du nettoyage des messages datant du mois dernier*/
int
dt=(
int
)DateTime.
Today.
Month-
1
;
//On supprime les tâches du mois précédent
cmd=
new
OleDbCommand
(
"Delete from planning where Format(PlanStartRange,'mm')<="
+
dt,
conn);
cmd.
ExecuteNonQuery
(
);
Console.
WriteLine
(
"Temporisation...CTRL+C pour arrêter le programme"
);
Thread.
Sleep
(
waitTime);
}
//Cette méthode ajoute le message "msg" dans le fichier log
private
void
logIt
(
string
msg)
{
if
(!
Directory.
Exists
(
"log"
))
{
Directory.
CreateDirectory
(
"log"
);
}
using
(
StreamWriter sw =
File.
AppendText
(
"log
\\
monitor.log"
))
{
sw.
WriteLine
(
DateTime.
Now+
": "
+
msg);
}
}
//Cette méthode envoie l'e-mail à l'utilisateur
//Vous devez bien sûr avoir un serveur SMTP
private
void
sendMsg
(
string
email,
string
body)
{
MailMessage msg =
new
MailMessage
(
);
msg.
From=
fMail;
msg.
To=
e-
mail;
msg.
Subject=
"Rappel de vos tâches"
;
msg.
Body=
body;
SmtpMail.
SmtpServer=
sSMTP;
Console.
WriteLine
(
DateTime.
Now+
": Envoi d'un e-mail à "
+
e-
mail);
try
{
SmtpMail.
Send
(
msg);
}
catch
(
Exception e)
{
logIt
(
e.
Message);
Environment.
Exit
(
1
);
}
}
}
class
startCheck
{
static
void
Main
(
)
{
checkAccess checker=
new
checkAccess
(
);
}
}