Créer et consommer un service web avec .NET

Dans cet article nous allons aborder de manière approfondie la gestion de services web en dotnet. Un exemple complet ainsi que les sources seront expliqués et disponibles en téléchargement. Nous verrons comment créer et consommer un service web en utilisant ASP.NET et C#. ?

- Titre : Créer et consommer un service web avec .NET

- Parution : 10 juin 2004

- Licence : Copyright ® 2004 Developpez. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Architecture des services web!

- I-B. Élaboration d'un service web simple

- I-C. Consommation d'un service web simple

- I-D. Les différents comportements possibles des services web

- I-E. Les appels asynchrones

- II. Notre petite application

- II-A. Le service web

- II-B. L'interface cliente (consommatrice du service web)

- II-C. Le démon

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();
}
}