Créer et Consommer un service web avec .NET
Date de publication : 10/06/2004
Par
Stéphane Eyskens (Autres articles)
Dans cet article nous allons aborder de manière approfondie la gestion de services web en dotnet. Un exemple complet ainsi
que les sources sera expliqué et disponible en téléchargement. Nous verrons comment créer et consommer un service web en utilisant
ASP.NET et C#.
1. Introduction
1.1. Architecture des services web!
1.2. Elaboration d'un service web simple
1.3. Consommation d'un service web simple
1.4. Les différents comportements possibles des services web
1.5. Les appels asynchrones
2. Notre petite application
2.1. Le service web
2.2. L'interface cliente (consommatrice du service web)
2.3. Le démon
3. Téléchargement
1. 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 Ms access
- Un démon se chargeant d'envoyer les e-mails aux utilisateurs et de nettoyer la base de donnée.
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. Etant 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.
1.1. Architecture des services web!
1.2. Elaboration d'un service web simple
Les services web sont des pages ASP.NET dont l'extension est asmx. A 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]
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. Si 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.
1.3. 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 trouve 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 appelera 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="c#" Codebehind="UtilisePetitExemple.aspx.cs" AutoEventWireup="true" Inherits="UtilisePetitExemple.ExempleConsommation" >
<HTML>
<body MS="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)
{
Exemple petitExemple=new Exemple();
reponse_webservice.Text=petitExemple.Additionne(5,10).ToString();
}
}
}
1.4. 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;
[WebMethod]
public void stockeValeurCliente(int valeurCliente)
{
a=valeurCliente;
}
[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();
petitExemple.stockeValeurCliente(3);
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, contraitement à un "dialogue" entre un objet classique et son instance,
le membre "a" ayant été affecté lors de l'appel à la 1ère 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 persistence, 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 pertinement 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ées 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 parralè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 communiation entre le
consommateur et le service. Il doit identifier votre méthode web de manière unique. Si 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
1.5. 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 commumnication
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)
{
IAsyncResult Reponse = petitExemple.BeginAdditionne(10,15,null,null);
Response.Write("Le traitement continue");
/**************************************************
* Ici vous pouvez exécuter n'importe quelle 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();
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)
{
AsyncCallback cb = new AsyncCallback(AdditionneCallback);
IAsyncResult Reponse=petitExemple.BeginAdditionne(10,15,cb,petitExemple);
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();
}
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};
Response.Write(petitExemple.EndAdditionne(appel1));
Response.Write(petitExemple.EndAdditionne(appel2));
}
}
}
2. 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.
2.1. 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 retourne 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
{
private string strConn
private bool initDb()
{
strConn="Provider=Microsoft.Jet.OLEDB.4.0;Data source="+Server.MapPath("db/planning.mdb");
conn=new OleDbConnection(strConn)
try
{
this.conn.Open()
}
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)
{
if(Lname==null || Fname==null || Email == null){return false;}
Regex CheckEmail = new Regex("^[a-z]{1,}@[a-z]{1,}\\.[a-z]{2,3}$");
if(!CheckEmail.IsMatch(Email))
{
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)
{
Session["User"]=u;
return true;
}
else
{
return false;
}
}
else
{
if(!newUser)
{
return false;
}
else
{
strSql="insert into Users(UserId,LastName,FirstName,Pwd,Email) ";
strSql+="values('"+u+"','"+Lname+"','"+Fname+"','"+p+"','"+Email+"')";
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;}
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;
}
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";
}
}
}
2.2. 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 sont effectuées 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["Email"]=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();
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();
CookieContainer checkCookie;
if (Session["checkCookie"] == null)
checkCookie= new CookieContainer();
else
checkCookie = (CookieContainer) Session["checkCookie"];
ws.CookieContainer = checkCookie;
if((bool)Session["NewUser"])
{
Response.Write("mail"+Session["Email"].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["Email"].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;
Heure.DataSource = ws.getHourList();
Heure.DataBind();
BindGrid();
}
}
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();
}
public void DgDelete(DataGridCommandEventArgs E)
{
info.Text=ws.deleteFromPlanning((int)dg.DataKeys[(int)E.Item.ItemIndex]);
BindGrid();
}
public void dg_PageIndexChanged(object sender, DataGridPageChangedEventArgs E)
{
dg.CurrentPageIndex=E.NewPageIndex;
BindGrid();
}
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!="")
{
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();
}
}
}
}
2.3. Le démon
Le démon se charge d'expédier les e-mails aux utilisateurs ayant demandé à être averti 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 si 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 rappellant la tâche en question.
*/
using System;
using System.IO;
using System.Threading;
using System.Web.Mail;
using System.Data.OleDb;
using Ayende
class checkAccess
{
private string dbF
private string fMail
private string sSMTP
static TimeSpan waitTime = new TimeSpan(0, 0, 59)
public checkAccess()
{
getInitParams()
Thread checkMail=null;
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();
while(true)
{
if(checkMail.Join(waitTime))
{
checkMail = new Thread(new ThreadStart(dbCheck));
Console.WriteLine(DateTime.Now+": Démarrage du thread "+checkMail.GetHashCode());
checkMail.Start();
}
}
}
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);
string sql="select task,lastName,firstname,email,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;
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é");
}
int dt=(int)DateTime.Today.Month-1;
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);
}
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);
}
}
private void sendMsg(string email,string body)
{
MailMessage msg = new MailMessage();
msg.From=fMail;
msg.To=email;
msg.Subject="Rappel de vos tâches";
msg.Body=body;
SmtpMail.SmtpServer=sSMTP;
Console.WriteLine(DateTime.Now+": Envoi d'un e-mail à "+email);
try
{
SmtpMail.Send(msg);
}
catch (Exception e)
{
logIt(e.Message);
Environment.Exit(1);
}
}
}
class startCheck
{
static void Main()
{
checkAccess checker=new checkAccess();
}
}
3. Téléchargement
Vous êtes bien sûr autorisé à manipuler l'application fournie comme bon vous semblera
Cet article est la propriété de www.developpez.com en tant qu'hebergeur ainsi que
celle de Stéphane Eyskens en tant que redacteur, ce texte est donc protégé par le code de la
propriété intellectuelle et est soumis à la réglementation en vigueur.
www.developpez.com ou son auteur se reserve le droit d'apporter des modifications
sans préavis. Vous pouvez utiliser cet article comme bon vous semble, faire un lien depuis
votre site Web, ou le copier en spécifiant l'auteur et la provenance (www.developpez.com)
Le non respect de cette règle equivaudrait à faire une contrefaçon. La responsabilité de
www.developpez.com, de l'un de ses membres, ou de la direction ne pourra etre engagé en cas
de destruction partielle ou totale des données ou de l'architecture système ou logicielle
inhérente à l'utilisation des ses logiciels. Les logiciels decrits ici sont la propriété de
leurs auteurs respectifs.
|