Gestion d'un formulaire OpenXML avec Windows Workflow Foundation

Florian Casabianca

Cet article a pour but de montrer une utilisation conjointe du format Open XML et de Windows Workflow Foundation à travers la création d'un Workflow de traitement de formulaires Microsoft Word.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Gestion d'un formulaire OpenXML avec Windows Workflow Foundation

- Auteur : Florian Casabianca

- Parution : 4 septembre 2008

- Mise à jour : 4 septembre 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2008 Florian Casabianca. 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. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- Introduction

- I. Création du formulaire

- I-A. Word 2007

- I-B. Mise en place du custom XML

- II. Le contrat de service

- III. Création du Workflow

- III-A. ReceiveActivity

- III-B. Développer ses propres activités

- III-B-1. ExtractCustomXmlActivity

- III-B-2. ValidationXDocumentActivity

- III-C. SaveCustomXmlActivity

- III-D. Utilisation des activités personnalisées

- III-D-1. Extraction du Custom XML

- III-D-2. Validation du Custom XML

- III-D-3. Sauvegarde du Custom XML

- III-D-4. Sauvegarde du Custom XML

- IV. L'hôte

- V. Le client

- Conclusion

- Sources

- Liens

- Remerciements

- Contact

Introduction

Cet article a pour but de montrer une utilisation conjointe du format Open XML et de Windows Workflow Foundation à travers la création d'un Workflow de traitement de formulaires Microsoft Word.

Nous allons aborder dans une première partie comment créer un document Word au format Open XML contenant directement des données métier. Cela passera par l'utilisation de Custom XML lié à des Content Controls (ou Structured Document Tags).

Nous passerons ensuite au développement d'un Workflow permettant de traiter ce document Open XML afin d'en extraire les données métier saisies par un utilisateur. Nous utiliserons pour cela le SDK Open XML permettant de manipuler plus facilement ce type de document. L'envoi du document au Workflow se fera via un service WCF. Nous découvrirons ainsi quelques nouveautés apportées à Windows Workflow 3.5 permettant son interaction avec WCF. Une grande partie sera aussi consacrée au développement d'activités personnalisées et réutilisables que nous intégrerons au Workflow.

I - Création du formulaire

Open XML intègre la notion de Custom XML part. Il s'agit de la possibilité d'insérer au sein d'un document Open XML des données métiers. Celles-ci seront stockées dans un fichier XML (une part) ne contenant aucune balise Open XML, seulement ses propres balises métier. Les informations contenues dans ce fichier pourront être liées à des Content Controls disposés au sein du document afin d'être visibles et manipulables par un utilisateur. Cette technique permet une séparation totale entre les données et leur présentation dans un document Open XML.

I-A - Word 2007

La première chose à faire est d'activer l'onglet Développeur si se n'est déjà fait.

Cliquez sur le bouton Microsoft Office

puis sur Options Word. La fenêtre suivante apparait :

Cochez la case Afficher l'onglet développeur dans le ruban, puis cliquez sur OK. Le nouvel onglet devrait apparaitre dans le ruban :

Il ne reste plus qu'à insérer les différents contrôles au sein du document. N'oubliez pas d'éditer les propriétés de ces contrôles et de leur assigner notamment un titre (visible par l'utilisateur dans Word) et un nom de balise (qui va nous permettre de différencier les différents contrôles de contenu lors de la phase de data binding).

Notre document final ressemblera à ceci :

Comme vous le voyez, il s'agit d'un formulaire simple de saisie d'informations concernant un animal quelconque.

I-B - Mise en place du custom XML

Quittons Word pour nous intéresser à la mise en place de la partie Custom XML (Custom XML part) au sein du document OpenXML.

Voici le XML métier que nous allons insérer dans le custom XML :

Fichier XML métier

<?xml version="1.0" encoding="utf-8" ?>
<ferme xmlns="http://developpez.com/ns/animauxdelaferme" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<animal id="">
<espece></espece>
<datenaissance></datenaissance>
<description></description>
<photo></photo>
<nom></nom>
</animal>
</ferme>

Et le schéma associé :

Schéma XML

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" attributeFormDefault="unqualified"
elementFormDefault="qualified" targetNamespace="http://developpez.com/ns/animauxdelaferme"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="ferme">
<xs:complexType>
<xs:sequence>
<xs:element name="animal">
<xs:complexType>
<xs:sequence>
<xs:element name="espece" type="xs:string" />
<xs:element name="datenaissance" type="xs:dateTime" />
<xs:element name="description" type="xs:string" />
<xs:element name="photo" type="xs:base64Binary" />
<xs:element name="nom" type="xs:string" />
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>

Nous voulons donc mettre en place un data binding (bidirectionnel) entre les Content Controls du document Word et notre XML métier. Pour cela nous allons utiliser un outil nommé Word 2007 Content Control Toolkit disponible sur le site Codeplex.

Son utilisation est assez simple. Après avoir ouvert le document Word (via le toolkit) vous vous retrouvez avec une interface ressemblant à celle ci-dessous :

Le panneau de gauche liste les différents Content Controls du document. La colonne Tag permet de différencier les Content Controls entre eux (d'où l'importance d'avoir rempli cette information dans les propriétés des Content Controls lors de leur insertion dans Word). Le panneau de droite permet de gérer les Custom XML du document. Notre document Word n'en contenant pas, il suffit de cliquer sur le lien « Click here to create a new one » pour en créer un.

Puis, dans l'onglet Edit View, saisissez le XML métier que vous souhaitez insérer dans le document :

Retournez maintenant dans l'onglet Bind View. Un treeview représentant votre document XML y apparait. Pour associer un nœud du document XML à un Content Control il suffit simplement d'effectuer un drag n' drop du nœud sur le Content Control en question (du panneau de gauche). Et c'est tout ! À noter que pour sélectionner un nœud il faut faire un double clic dessus. Vous devriez voir la colonne XPath se remplir avec les informations du binding. Enfin, n'oubliez pas d'enregistrer les modifications avant de quitter.

Si l'on examine le fichier OpenXML après cette opération, on s'aperçoit que celui-ci contient un nouveau répertoire (customXML) à sa racine :

Le fichier Item1.xml contient exactement (ni plus, ni moins) le code XML que nous avons inséré via le Content Control Toolkit. Un deuxième fichier itemProps1.xml a aussi été créé. Celui-ci contient des informations concernant le custom XML associé. Dans le cas de notre exemple, voici son contenu :

Contenu du fichier de propriété associé au Custom XML

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<ds:datastoreItem ds:itemID="{8DBF1AED-BD48-42B0-99A6-890F79C2D5FF}"
xmlns:ds="http://schemas.openxmlformats.org/officeDocument/2006/customXml">
<ds:schemaRefs>
<ds:schemaRef ds:uri="http://developpez.com/ns/animauxdelaferme"/>
</ds:schemaRefs>
</ds:datastoreItem>

Deux éléments sont importants ici :

- l'identifiant (itemID) qui permet d'identifier de façon unique une partie au sein du document OpenXML. Word utilise un GUID, mais vous pouvez utiliser n'importe quel type d'identifiant ;

- une référence sur le schéma utilisé dans le Custom XML.

Les noms des fichiers (item1.xml et itemProps1.xml) sont totalement arbitraires. Rien ne vous empêche de les changer (il ne faut donc pas se baser dessus pour récupérer les informations qu'ils contiennent). Ce qui importe ce sont les relations qui lient les parties d'un document OpenXML entre elles, pas leur nom.

Si l'utilisateur remplit le document de cette manière…

… le contenu du Custom XML sera le suivant :

Contenu du custom XML

<?xml version="1.0" encoding="utf-8"?><ferme xmlns="http://developpez.com/ns/animauxdelaferme" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<animal id="000339">
<espece>JERSEY</espece>
<datenaissance>2006-08-11</datenaissance>
<description>Mathilde a la réputation de séduire tous les membres de la famille par sa personnalité attachante, sa docilité et sa présence.
Une vache de petite taille, fort charmante avec son pelage fauve.</description>
<photo>.DAAYEBQYFBA.</photo>
<nom>Mathilde</nom>
</animal>
</ferme>

Les informations contenues dans le formulaire sont donc clairement dissociées du document Word et se trouvent au sein d'un fichier XML « métier » totalement personnalisé. L'utilisateur va maintenant pouvoir envoyer ce formulaire à un Workflow qui va extraire le Custom XML part du document OpenXML pour en récupérer les informations (et les traiter selon sa logique métier).

Note : peut-être vous demandez vous ce qu'une vache vient faire ici ? Rassurez-vous, l'auteur ne le sait pas lui-même…

II - Le contrat de service

Le Workflow sera exposé en tant que service WCF afin que l'utilisateur puisse lui transmettre le document à traiter. Comme tout service WCF nous devons donc commencer par définir le contrat de service utilisé.

Dans la solution Visual Studio créez un nouveau projet de type Librairie de classe et nommez-le FileUploadServiceLibrary. Ajoutez les références vers System.Runtime.Serialization et System.ServiceModel. Créez une nouvelle interface IUploadService et définissez-y une fonction UploadFile prenant en paramètre un tableau d'octets et renvoyant un objet de type ResultatTraitement :

Contrat de service

using System.IO;
using System.ServiceModel;
using System.Runtime.Serialization;
using System;

namespace FileUploadServiceLibrary
{
[ServiceContract]
public interface IUploadService
{
[OperationContract]
ResultatTraitement UploadFile(byte[] file);
}

[DataContract]
public class ResultatTraitement
{
[DataMember]
public Guid NumDossier { get; set; }

[DataMember]
public bool Accepte { get; set; }

[DataMember]
public String Commentaire { get; set; }
}
}

N'oubliez pas les habituels attributs ServiceContract, OperationContract, DataContract et DataMember.

La classe ResultatTraitement permettra au client de connaitre le résultat du traitement par le Workflow du fichier uploadé.

III - Création du Workflow

Plusieurs templates sont disponibles pour le type de projet Workflow. Ici nous utiliserons le template Sequential Workflow Library permettant de définir un Workflow séquentiel, c'est-à-dire contenant des activités s'exécutant dans un ordre défini. Étant donné qu'il s'agit d'une librairie, il nous faudra développer (dans une prochaine partie) un hôte permettant de l'héberger. Appelons ce projet WorkflowFormulaireLibrary.

Le template contient normalement déjà un Workflow appelé Workflow1. Renommez-le en WorkflowTraitementFormulaire. Ajoutez une référence vers le contrat de service que nous avons créé précédemment :

En mode design le Workflow ressemble à ceci :

III-A - ReceiveActivity

La version 3.5 du Framework apporte deux nouvelles activités à Workflow Foundation : ReceiveActivity et SendActivity. Ces deux nouvelles activités permettent la prise en charge de services WCF.

SendActivity est utilisée pour appeler un service WCF depuis un Workflow et ReceiveActivity permet d'implémenter une opération définie dans un contrat de service (et ainsi exposer le Workflow en tant que service WCF). ReceiveActivity peut implémenter une opération d'un contrat de service existant (c'est ce que nous allons faire) ou définir le contrat et l'opération directement dans le Workflow.

Ajoutons donc une activité de type ReceiveActivity au sein du Workflow via un simple drag n' drop et nommons-la WorkflowTraitementFormulaire. Dans la fenêtre de propriétés, mettez à True la propriété CanCreateInstance.

Cette propriété spécifie qu'un appel d'un client à cette opération de service déclenchera la création d'un nouveau Workflow (et son exécution).

Notre Workflow doit maintenant ressembler à ceci :

Vous aurez noté la petite bulle rouge indiquant une erreur au niveau de l'activité ReceiveActivity. Un clic dessus vous apportera plus de détails :

L'erreur indique qu'il n'existe pas d'opération de service (WCF) associée à cette activité. Qu'à cela ne tienne, ajoutons-en une. Dans la fenêtre de propriétés de l'activité ReceiveActivity localisez la propriété ServiceOperationInfo et cliquez sur les trois petits points à côté :

Une nouvelle fenêtre s'ouvre vous permettant d'ajouter ou d'importer un contrat existant. Nous choisissons ici l'option d'import :

Une deuxième fenêtre apparait demandant le contrat de service à importer. Dans le nœud Referenced Assemblies vous devriez trouver le contrat WCF que nous avons créé précédemment (dans le cas contraire recompilez la solution). Sélectionnez-le et validez.

La première fenêtre réapparaît. Les différentes opérations du contrat sont listées (dans notre cas nous n'en avons qu'une). Sélectionnez l'opération à associer à l'activité et validez.

La propriété ServiceOperationInfo devrait maintenant être correctement renseignée et la bulle d'erreur devrait avoir disparu.

Notre Workflow est maintenant capable de recevoir un appel d'un client à la méthode UploadFile. Il nous faut ensuite pouvoir récupérer le fichier envoyé (c'est-à-dire le tableau d'octets) et retourner un objet de type ResultatTraitement au client. Pour cela nous allons déclarer deux propriétés dans le fichier de code associé au Workflow :

Propriétés du Workflow

//Fichier sous forme de tableau de bytes envoyé par le client
private byte[] _fileBytes;
public byte[] FileBytes
{
get
{
return _fileBytes;
}
set
{
_fileBytes = value;
}
}

//Objet décrivant le résultat du traitement
private ResultatTraitement _resultatWorkflow;
public ResultatTraitement ResultatWorkflow
{
get
{
return _resultatWorkflow;
}
set
{
_resultatWorkflow = value;
}
}

L'astuce consiste maintenant à lier (binding) ces propriétés à la méthode UploadFile. Pour cela, retournez en mode design et rendez-vous dans la fenêtre de propriétés de l'activité ReceiveActivity. Une catégorie Parameters a fait son apparition lorsque vous avez associé l'activité à la méthode UploadFile :

Nous allons lier ces paramètres aux propriétés créées précédemment. Faites un double clic sur le petit cylindre jaune pour afficher la fenêtre suivante :

La liste des propriétés disponibles pour une liaison apparait. Associez la valeur de retour à ResultatUpload et file à FileBytes.

La propriété FileBytes du Workflow sera dorénavant initialisée avec le paramètre de la fonction UploadFile et la valeur de retour de cette fonction sera la valeur de la propriété ResultatTraitement. Reste maintenant à effectuer des traitements sur le fichier reçu et à initialiser la propriété ResultatTraitement avec un objet de type ResultatTraitement en fonction de ces traitements.

L'activité ReceiveActivity est dite composite, c'est-à-dire qu'elle peut contenir d'autres activités. Nous allons donc y ajouter une activité de type CodeActivity. Il s'agit d'une activité standard de Workflow Foundation permettant d'exécuter du code. Dans la suite nous ajouterons d'autres activités, toujours à l'intérieur de l'activité ReceiveActivity. Cela veut dire que l'activité ReceiveActivity ne sera terminée (et donc renverra une réponse au client) que lorsque toutes les activités qu'elle contient le seront également. Cela signifie aussi que le client restera bloqué sur l'appel de la fonction UploadFile tant que l'activité ReceiveActivity ne sera pas terminée. Il s'agit d'un comportement qui n'est pas forcément souhaitable dans certaines situations (en particulier lorsque l'activité dure longtemps). Une alternative possible est d'utiliser la propriété IsOneWay de WCF sur la méthode UploadFile pour qu'elle ne renvoie pas de réponse. Le client devra alors effectuer un deuxième appel sur une autre fonction de service afin d'obtenir le résultat du traitement. Vous pouvez aussi utiliser l'activité SendActivity pour que le Workflow appelle un service côté client pour lui envoyer le résultat.

Une fois l'activité CodeActivity ajoutée et renommée en TraitementUploadFile, le Workflow devrait ressembler à ceci :

Effectuez un double clic sur l'activité CodeActivity pour y insérer le code suivant :

TraitementUploadFile

private void TraitementUploadFile_ExecuteCode(object sender, EventArgs e)
{
Console.Out.WriteLine("Réception d'un fichier");

try
{
//on crée le répertoire
if (!Directory.Exists(this.FolderPathToSaveDocs))
Directory.CreateDirectory(this.FolderPathToSaveDocs);

Console.WriteLine("Enregistrement du fichier sous: {0}", this.FormulairePath);
//on enregistre le fichier sur le disque
File.WriteAllBytes(FormulairePath, this.FileBytes);

Console.WriteLine();
Console.WriteLine("Fichier {0} enregistré", this.FormulairePath);
}
catch (IOException ex)
{
Console.WriteLine(
String.Format("An exception was thrown while opening or writing to file {0}", this.FormulairePath));
Console.WriteLine("Exception is: ");
Console.WriteLine(ex.ToString());

throw ex;
}
}

Ce code crée tout d'abord un répertoire qui contiendra les différents fichiers créés par le Workflow. Puis il y enregistre le formulaire envoyé par le client. Le formulaire (envoyé sous forme de tableau d'octets) est accessible via la propriété FileBytes (il s'agit de la propriété que nous avons liée au paramètre file de la méthode UploadFile).

Les propriétés FolderPathToSaveDocs et FormulairePath utilisées sont aussi définies dans le code associé au Workflow :

Propriétés du Workflow

//chemin du répertoire où enregistrer les différents fichiers
private string _folderPathToSaveDocs;
public string FolderPathToSaveDocs
{
get
{
if (_folderPathToSaveDocs == null)
_folderPathToSaveDocs = String.Format("formulaire_{0}", DateTime.Now.ToString("dd-MM-yyyy__HH_mm_ss"));
return _folderPathToSaveDocs;
}
}

//chemin où a été enregistré le formulaire envoyé par le client
private string _formulairePath;
public string FormulairePath
{
get
{
if (_formulairePath == null)
_formulairePath = Path.Combine(FolderPathToSaveDocs, "formulaire.docx");
return _formulairePath;
}
}

Nous allons aussi définir deux autres propriétés qui nous serviront pour plus tard : SchemaPath qui contient l'emplacement du schéma XML associé au Custom XML et SchemaRef qui défini la référence de ce schéma XSD :

//fichier xsd permettant de valider le custom XML
private string _schemaPath = Path.Combine(System.Environment.CurrentDirectory, "Animal.xsd");
public string SchemaPath
{
get
{
return _schemaPath;
}
}

//référence du schéma du customXML
private string _schemaRef = @"http://developpez.com/ns/animauxdelaferme";
public string SchemaRef
{
get
{
return _schemaRef;
}
}

III-B - Développer ses propres activités

Nous venons de voir qu'il était possible d'ajouter du code à un Workflow via l'activité Code. Il existe une deuxième technique qui consiste à développer ses propres activités encapsulant une logique métier (dans le même esprit que le développement de contrôles utilisateur en Winform par exemple). Bien que cela demandera du travail supplémentaire, créer ses propres activités offrira de nombreux avantages :: composants indépendants (plus facilement testables) et réutilisables dans d'autres Workflows, disponibilité de ces composants dans la boite à outils de Visual Studio (utilisation via un simple drag n' drop). Ce dernier point est important, car il va potentiellement offrir à une personne non développeur (mais qui connait le métier) de construire de A à Z un Workflow simplement en manipulant des activités via la surface de design (à la manière de mashups).

Pour notre exemple nous allons développer trois activités :

- ExtractCustomXmlActivity dont le but sera d'extraire le Custom XML d'un document OpenXML correspondant à un schéma donné ;

- ValidationXDocumentActivity qui devra valider le contenu d'un XDocument par rapport à un schéma XSD ;

- SaveCustomXmlActivity qui sauvegardera le contenu du Custom XML sur le disque dur.

III-B-1 - ExtractCustomXmlActivity

Dans la solution Visual Studio rajoutez un nouveau projet de type Workflow Activity Library et nommez-le CustomActivities.

Cette librairie contiendra les trois activités que nous allons développer. Comme vous le voyez, il s'agit d'une librairie indépendante du projet contenant notre Workflow et donc réutilisable dans d'autres Workflows.

ExtractCustomXmlActivity devra ouvrir un document OpenXML (qui se trouve sur le disque dur) et en extraire le custom XML correspondant à un schéma donné. Remarquez que nous ne parlons pas d'extraire le custom XML spécifique au formulaire de notre exemple, mais d'un Custom XML correspondant à un schéma donné. Cela permettra de réutiliser cette activité pour extraire le Custom XML de n'importe quel autre document OpenXML, même s'il ne s'agit pas d'un formulaire correspondant à notre exemple.

Bien que l'extraction de Custom XML puisse être faite de différentes manières, nous allons utiliser ici le SDK OpenXML en version 1.0. Il s'agit d'une API utilisant la librairie System.IO.Packaging, mais offrant un niveau d'abstraction supplémentaire et donc une manipulation plus facile de documents OpenXML. À titre de comparaison vous pouvez jeter un coup d'oil sur l'article suivant : lire et modifier un fichier Word OpenXML utilisant System.IO.Packaging pour manipuler les documents OpenXML.

Il nous faut rajouter une référence vers le composant DocumentFormat.OpenXml :

Visual Studio a normalement créé automatiquement une nouvelle activité appelée Activity1. Supprimez-la et ajoutez-en une nouvelle (clic droit, Add, Activity) ou alors renommez-la en ExtractCustomXmlActivity.

Si vous analysez le code de cette activité, vous verrez qu'elle dérive de la classe de base SequenceActivity. Cela signifie que notre activité est de type composite et a la possibilité d'héberger d'autres activités en son sein. Dans notre exemple nous ne voulons développer que des activités simples et n'avons donc pas besoin de cette possibilité de composition. Nous allons donc plutôt faire dériver l'activité de la classe Activity :

public partial class ExtractCustomXmlActivity : Activity

Cette activité doit donc prendre en entrée un chemin vers un fichier OpenXML et une référence de schéma (tous deux de type String) et retourner le Custom XML correspondant (de type XDocument). Pour cela nous allons utiliser des dependency properties. L'avantage des dependency properties est qu'elles vont permettre de faire des liaisons (binding) entre activités. Il sera par exemple possible de lier une propriété d'entrée d'une activité à une propriété de sortie d'une autre activité.

Dans le cas de l'activité ExtractCustomXmlActivity nous avons besoin de trois dependency properties :

Dependency propertie pour le chemin du fichier OpenXML

public static DependencyProperty FilePathProperty = DependencyProperty.Register("FilePath", typeof(string), typeof(ExtractCustomXmlActivity));

[DescriptionAttribute("Chemin du fichier OpenXML dont il faut extraire le custom Xml")]
[CategoryAttribute("Input")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public string FilePath
{
get
{
return ((string)(base.GetValue(ExtractCustomXmlActivity.FilePathProperty)));
}
set
{
base.SetValue(ExtractCustomXmlActivity.FilePathProperty, value);
}
}

Dependency propertie pour le Custom XML

public static DependencyProperty CustomXmlProperty = DependencyProperty.Register("CustomXml", typeof(XDocument), typeof(ExtractCustomXmlActivity));

[DescriptionAttribute("XDocument résultant de l'extraction du custom Xml")]
[CategoryAttribute("Output")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public XDocument CustomXml
{
get
{
return ((XDocument)(base.GetValue(ExtractCustomXmlActivity.CustomXmlProperty)));
}
set
{
base.SetValue(ExtractCustomXmlActivity.CustomXmlProperty, value);
}
}

Dependency propertie pour le schéma XML

public static DependencyProperty SchemaProperty = DependencyProperty.Register("Schema", typeof(string), typeof(ExtractCustomXmlActivity));

[DescriptionAttribute("Schema associé au custom Xml à extraire")]
[CategoryAttribute("Input")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public string Schema
{
get
{
return ((string)(base.GetValue(ExtractCustomXmlActivity.SchemaProperty)));
}
set
{
base.SetValue(ExtractCustomXmlActivity.SchemaProperty, value);
}
}

L'écriture d'une dependency propertie peut sembler quelque peu rébarbative, mais Visual Studio propose un snipet pour vous aider.

Il ne reste plus qu'à implémenter la logique de l'activité. Cela s'effectue en implémentant la méthode Execute qui est une méthode virtuelle de la classe de base.

Méthode Execute de l'activity ExtractCustomXmlActivity

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Console.WriteLine("Extraction du custom XML");

XNamespace ds = @"http://schemas.openxmlformats.org/officeDocument/2006/customXml";
XNamespace nsmetier = Schema;

using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(FilePath, false))
{
MainDocumentPart mainPart = wordDoc.MainDocumentPart;
foreach (var customXmlPart in mainPart.CustomXmlParts)
{
//on ouvre le CustomXmlPropertiesPart associé au customXmlPart qu'on traite
XDocument customXmlPropertiesPart = XDocument.Load(XmlReader.Create(customXmlPart.CustomXmlPropertiesPart.GetStream()));
//namespace du schéma du Custom XML
string uri = customXmlPropertiesPart.Descendants(ds + "schemaRef").First().Attribute(ds + "uri").Value;
//si le namespace correspond à celui que l'on recherche
if (uri.Equals(Schema, StringComparison.InvariantCultureIgnoreCase))
{
CustomXml = XDocument.Load(XmlReader.Create(customXmlPart.GetStream()));
break;
}
}

}
return base.Execute(executionContext);
}

C'est ici que nous utilisons le SDK OpenXML. Nous instancions un objet de type WordprocessingDocument qui représente un document Word OpenXML. La propriété MainDocumentPart renvoie la partie principale de ce document (notez la simplicité par rapport à l'utilisation directe de System.IO.Packaging). Nous parcourons la liste des Custom XML et pour chacun d'eux nous analysons la partie contenant les propriétés associées à la recherche de l'espace de nom passé en paramètre de l'activité. Une fois celle-ci trouvée, nous chargeons le Custom XML dans un XDocument et l'affectons à la propriété CustomXML de l'activité.

La méthode Execute doit retourner une valeur de l'énumération ActivityExecutionStatus. L'instruction base.Execute(executionContext); retourne la valeur Closed qui spécifie que l'activité est terminée.

III-B-2 - ValidationXDocumentActivity

Dans notre librairie CustomActivities ajoutez une nouvelle activité et nommez-la ValidationXDocumentActivity (pensez aussi à modifier la classe de base comme pour l'activité précédente). Cette activité devra valider un document XML par rapport à un schéma XSD. Un booléen déterminera si la validation a réussi ou non.

Déclarons trois dependency properties :

SchemaPathProperty de type string représentera le chemin du fichier XSD.

Dependency property pour le chemin du fichier xsd

public static DependencyProperty SchemaPathProperty = DependencyProperty.Register("SchemaPath", typeof(string), typeof(ValidationXDocumentActivity));

[DescriptionAttribute("Chemin du fichier xsd permettant la validation du custom XML")]
[CategoryAttribute("Input")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public string SchemaPath
{
get
{
return ((string)(base.GetValue(ValidationXDocumentActivity.SchemaPathProperty)));
}
set
{
base.SetValue(ValidationXDocumentActivity.SchemaPathProperty, value);
}
}

XDocumentToValidateProperty sera le XDocument à valider.

Dependency property pour le XDocument à valider

public static DependencyProperty XDocumentToValidateProperty =
DependencyProperty.Register("XDocumentToValidate", typeof(XDocument), typeof(ValidationXDocumentActivity));

[DescriptionAttribute("XDocument contenant le custom Xml à valider")]
[CategoryAttribute("Input")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public XDocument XDocumentToValidate
{
get
{
return ((XDocument)(base.GetValue(ValidationXDocumentActivity.XDocumentToValidateProperty)));
}
set
{
base.SetValue(ValidationXDocumentActivity.XDocumentToValidateProperty, value);
}
}

Enfin, IsCustomXmlValideProperty, de type booléen, sera le résultat de la validation.

Dependency property pour le résultat de la validation

public static DependencyProperty IsCustomXmlValideProperty =
DependencyProperty.Register("IsCustomXmlValide", typeof(bool), typeof(ValidationXDocumentActivity));

[DescriptionAttribute("Résultat de la validation")]
[CategoryAttribute("Output")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public bool IsCustomXmlValide
{
get
{
return ((bool)(base.GetValue(ValidationXDocumentActivity.IsCustomXmlValideProperty)));
}
set
{
base.SetValue(ValidationXDocumentActivity.IsCustomXmlValideProperty, value);
}
}

Implémentons maintenant la logique de l'activité via la méthode Execute :

Méthode Execute de ValidationXDocumentActivity

/// <summary>
/// Valide le custom Xml du document
/// </summary>
/// <param name="executionContext"></param>
/// <returns></returns>
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Console.WriteLine("Validation du custom XML");

if (XDocumentToValidate != null)
{
try
{
//on charge le fichier xsd
XmlSchemaSet xmlSchemaSet = LoadSchema(SchemaPath);

XDocumentToValidate.Validate(xmlSchemaSet, null);
IsCustomXmlValide = true;
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
return base.Execute(executionContext);
}

Nous chargeons le fichier XSD via la méthode LoadSchema (voir plus bas). Celle-ci retourne un objet de type XmlSchemaSet qui est un cache pouvant contenir plusieurs schémas XSD. Nous utilisons ensuite la méthode d'extension Validate (de l'espace de nom System.Xml.Schema) sur le XDocument contenant le Custom XML. Si la validation échoue, une exception est lancée. Si la validation réussit, nous mettons la valeur de la propriété IsCustomXmlValide à True.

Voici le code de la méthode LoadSchema :

Fonction LoadSchema

static XmlSchemaSet LoadSchema(string schemaPath)
{
XmlSchemaSet xmlSchemaSet = new XmlSchemaSet(new NameTable());
using (XmlReader reader = XmlReader.Create(schemaPath))
{
//chargement du schéma
XmlSchema animauxSchema = XmlSchema.Read(reader, null);
reader.Close();
//ajout du schéma au XmlSchemaSet
xmlSchemaSet.Add(animauxSchema);
}
return xmlSchemaSet;
}

III-C - SaveCustomXmlActivity

Créons maintenant la troisième activité (vous savez comment faire maintenant). Celle-ci devra enregistrer sur le disque dur (à un emplacement déterminé par une dependency property) le Custom XML (dependency property également). De plus l'image sérialisée dans le Custom XML sera extraite et sauvegardée dans un fichier à part. Nous pourrions imaginer des scénarios plus élaborés comme l'enregistrement dans une base de données où l'envoi d'informations à un Web service.

La première dependency property, de type XDocument représente le Custom XML :

Property dependency pour XDocument à sauver

public static DependencyProperty CustomXmlToSaveProperty = DependencyProperty.Register("CustomXmlToSave", typeof(XDocument), typeof(SaveCustomXmlActivity));

[DescriptionAttribute("XDocument contenant le CustomXml à sauver")]
[CategoryAttribute("Input")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public XDocument CustomXmlToSave
{
get
{
return ((XDocument)(base.GetValue(SaveCustomXmlActivity.CustomXmlToSaveProperty)));
}
set
{
base.SetValue(SaveCustomXmlActivity.CustomXmlToSaveProperty, value);
}
}

La deuxième correspond au chemin où sauvegarder le Custom XML ainsi que l'image désérialisée :

Property dependency pour le chemin où sauver les images

public static DependencyProperty SavePathProperty = DependencyProperty.Register("SavePath", typeof(string), typeof(SaveCustomXmlActivity));

[DescriptionAttribute("Chemin où sauver le customXml")]
[CategoryAttribute("Input")]
[BrowsableAttribute(true)]
[DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)]
public string SavePath
{
get
{
return ((string)(base.GetValue(SaveCustomXmlActivity.SavePathProperty)));
}
set
{
base.SetValue(SaveCustomXmlActivity.SavePathProperty, value);
}
}

Enfin l'habituelle fonction Execute :

Fonction Executede SaveCustomXmlActivity

/// <summary>
/// Enregistre le custom Xml
/// </summary>
/// <param name="executionContext"></param>
/// <returns></returns>
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Console.WriteLine("Sauvegarde du custom XML");

if (CustomXmlToSave != null)
{
XNamespace ns = @"http://developpez.com/ns/animauxdelaferme";

CustomXmlToSave.Save(System.IO.Path.Combine(SavePath, "customXML.xml"));

//élément photo
XElement binaryData = CustomXmlToSave.Descendants(ns + "photo").Single();
//conversion en tableau de bytes
byte[] buffer = Convert.FromBase64String(binaryData.Value);
//sauvegarde de l'image sur le disque
using (System.IO.FileStream stream = System.IO.File.Create(System.IO.Path.Combine(SavePath, "photo.jpg")))
{
stream.Write(buffer, 0, buffer.Length);
}
}

return base.Execute(executionContext);
}

Nous appelons la méthode Save du XDocument et lui passons en paramètre le nom du fichier où enregistrer son contenu. Nous convertissons ensuite l'image sérialisée en tableau d'octets dont nous enregistrons le contenu dans un fichier photo.jpg via un objet FileStream.

III-D - Utilisation des activités personnalisées

Maintenant que nous avons développé nos activités personnalisées, il est temps de les utiliser au sein de notre Workflow. Dans le projet WorkflowFormulaireLibrary ajoutez une référence à la librairie CustomActivities. En mode design, vous devriez voir apparaitre les trois activités au sein de la toolbox de Visual Studio :

Si elles n'apparaissent pas, recompilez un coup la solution.

Nos activités sont maintenant manipulables par drag n' drop et configurables via la fenêtre de propriétés de Visual Studio, sympa non ?

III-D-1 - Extraction du Custom XML

Maintenant que nous avons récupéré le formulaire envoyé par l'utilisateur nous allons en extraire le Custom XML renfermant les informations qui nous intéressent. Il est donc temps d'utiliser la première activité personnalisée que nous avons créée : ExtractCustomXmlActivity. Pour cela, rien de plus simple : faites un drag n' drop de cette l'activité (que nous nommerons RecupCustomXml) à la suite de l'activité CodeActivity :

Il faut ensuite configurer l'activité insérée via ses dependency properties que nous avions créées. Dans la fenêtre des propriétés, faites une liaison (binding) entre la propriété FilePath de l'activité et la propriété FormulairePath du Workflow puis entre la propriété Schema de l'activité et la propriété SchemaRef du formulaire. Nous ne nous préoccupons pas de la propriété CustomXml pour le moment.

Nous venons de rajouter une étape supplémentaire dans notre Workflow sans taper une seule ligne de code, mais simplement en effectuant un drag n' drop d'une activité et en liant ses propriétés à d'autres.

III-D-2 - Validation du Custom XML

Le custom XML étant extrait, la prochaine étape est sa validation. Rajoutons donc une activité ValidationXDocumentActivity que nous nommerons ValidationCustomXml :

Via la fenêtre des propriétés, nous allons lier la propriété SchemaPath à la propriété SchemaPath du Workflow et la propriété XDocumentToValidate à la propriété CustomXml de l'activité ExtractCustomXmlActivity que nous avons insérée précédemment.

L'activité ValidationXDocumentActivity va ainsi récupérer le document XML à valider directement auprès de l'activité ExtractCustomXmlActivity et le schéma à utiliser auprès du Workflow. Encore une nouvelle étape ajoutée au Workflow sans saisir la moindre ligne de code.

III-D-3 - Sauvegarde du Custom XML

Une fois l'étape de validation effectuée, la propriété IsCustomXmlValide de l'activité ValidationXDocumentActivity contient un booléen indiquant si le Custom XML est valide ou non. S'il est valide nous voudrions effectuer certaines opérations, s'il ne l'est pas nous voudrions en effectuer d'autres. Workflow Foundation propose une activité de base qui va nous permettre de réaliser cela : l'activité ifElseActivity.

Cette activité est composée de plusieurs branches, chacune étant associée à une condition. Son principe de fonctionnement est le suivant : la condition associée à la branche la plus à gauche est évaluée en premier. Si elle est évaluée à vrai alors les activités de cette branche sont exécutées (et les autres branches sont ignorées). Si la condition n'est pas évaluée à vrai alors on passe à la branche suivante et ainsi de suite. La branche la plus à droite n'a pas obligation à être associée à une condition. Dans ce cas c'est cette branche qui sera exécutée si les conditions des autres branches sont toutes égales à faux.

Une fois l'activité ifElseActivity ajoutée, la Workflow devrait ressembler à ceci :

La branche de gauche sera exécutée si le Custom XML est valide, celle droite s'il ne l'est pas.

Une petite bulle rouge nous indique l'erreur suivante :

Aucune condition n'a encore été associée à la branche de gauche. Pour corriger cela, rendez-vous dans la fenêtre de propriété de l'activité. La première chose à faire est de spécifier le type de condition : code ou déclarative. Vous pouvez en effet choisir de saisir la condition via du code (au sein d'une fonction qui renverra un booléen) ou bien de façon déclarative. Avec cette dernière technique les conditions seront sérialisées dans un fichier .rules intégré au Workflow en tant que ressource. L'avantage de cette approche est de pouvoir modifier dynamiquement les conditions alors que le Workflow est en cours d'exécution.

Deux propriétés font alors leur apparition dans la fenêtre :

Saisissez un nom pour la règle (chaque règle doit avoir un nom unique) puis son expression (en cliquant sur les trois petits points)

La saisie de la condition est facilitée par l'IntelliSence :

Voici la condition finale :

Ajoutez maintenant une activité CodeActivity dans chaque branche de l'activité ifElseActivity. Le Workflow ressemble maintenant à ceci :

Si le Custom XML est valide alors nous appellerons le code suivant :

Custom XML non valide

private void codeCustomXmlValide_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Le custom XML est valide");
this.ResultatWorkflow = new ResultatTraitement
{
Accepte = true,
NumDossier = Guid.NewGuid(),
Commentaire = "Formulaire accepté"
};
}

Nous initialisons la propriété ResultatWorkflow avec un nouvel objet de type ResultatTraitement.

Si le Custom XML n'est pas valide alors nous exécuterons le code suivant ::

Custom XML non valide

private void codeCustomXmlPasValide_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Le customXml n'est pas valide");
this.ResultatWorkflow = new ResultatTraitement
{
Accepte = false,
NumDossier = Guid.NewGuid(),
Commentaire = "Le customXml n'est pas valide"
};
}

III-D-4 - Sauvegarde du Custom XML

La dernière étape est la sauvegarde du Custom XML. Celle-ci ne s'effectue que si le Custom XML est valide. Ainsi nous allons ajouter une activité de type SaveCustomXmlActivity dans la branche de gauche de l'activité ifElseActivity :

Configurons l'activité en liant sa propriété CustomXmlToSave à la propriété CustomXml de l'activité RecupCustomXml et sa propriété SavePath à la propriété FolderPathToSaveDocs du Workflow:

Cette dernière activité marque la fin de notre Workflow. Nous allons maintenant nous pencher sur son hébergement.

IV - L'hôte

Le Workflow que nous venons de construire n'est qu'une librairie et il a donc besoin d'un hôte pour s'exécuter. IIS serait un choix idéal pour notre Workflow (d'autant plus qu'il est exposé en tant que service WCF), mais par souci de simplicité nous allons utiliser une simple application console. Il nous faudra cependant écrire nous même le code d'instanciation et d'hébergement de l'hôte.

Nous créons donc une application console et ajoutons les références nécessaires : System.ServiceModel (pour la partie WCF), System.Workflow.Activities, System.Workflow.ComponentModel, System.Workflow.Runtime, System.WorkflowServices et WorkflowFormulaireLibrary (qui contient notre Workflow).

Code de l'hôte

static void Main(string[] args)
{
try
{
WorkflowServiceHost workflowHost = new WorkflowServiceHost(typeof(WorkflowFormulaireLibrary.WorkflowTraitementFormulaire));

workflowHost.Description.Behaviors.Find<WorkflowRuntimeBehavior>().WorkflowRuntime.WorkflowTerminated
+= (sender, e) => { Console.WriteLine("WorkflowTerminated: " + e.Exception.Message); };

workflowHost.Description.Behaviors.Find<WorkflowRuntimeBehavior>().WorkflowRuntime.WorkflowCompleted
+= (sender, e) => { Console.WriteLine("WorkflowCompleted: " + e.WorkflowInstance.InstanceId.ToString()); };

workflowHost.Open();

Console.WriteLine("WorkWorkflowServiceHost démarré");
Console.ReadKey();

workflowHost.Close();
}
catch (Exception ex)
{
Console.Write(String.Format("Erreur: {0}", ex.Message));
Console.ReadLine();
}
}

Nous commençons par créer une instance de la classe WorkflowServiceHost. Cette dernière, qui fait son apparition dans le Framework 3.5, permet d'héberger un runtime Workflow Foundation au sein d'un service WCF. Nous lui passons en paramètre le type de notre Workflow.

Nous accédons ensuite à l'instance du WorkflowRuntime associée au WorkflowServiceHostet nous nous abonnons successivement aux évènements WorkflowTerminated et WorkflowCompleted. Le premier est déclenché quand le Workflow se termine suite à une erreur tandis que le dernier est déclenché si le Workflow se termine normalement.

Puis nous appelons la méthode Open et commençons l'écoute de messages. L'instruction Console.ReadKey(); permet de bloquer la console et laisser vivre le Workflow (technique habituelle pour les démonstrations à l'aide d'une application console.). Avant de quitter nous n'oublions pas d'appeler la méthode Close.

Il nous reste maintenant à spécifier la configuration WCF dans le fichier de configuration de l'hôte :

Configuration de l'hôte

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<services>
<service name="WorkflowFormulaireLibrary.WorkflowTraitementFormulaire">
<host>
<baseAddresses>
<add baseAddress = "http://localhost:8731/Design_Time_Addresses/FileUploadServiceLibrary/" />
</baseAddresses>
</host>
<endpoint bindingConfiguration="BindingConfig"
address ="UploadService"
binding="wsHttpContextBinding"
contract="FileUploadServiceLibrary.IUploadService">
</endpoint>
</service>
</services>
<bindings>
<wsHttpContextBinding>
<!-- Taille fixé à 5 MB -->
<binding name="BindingConfig"
messageEncoding="Mtom"
maxReceivedMessageSize="5000000">
<readerQuotas maxArrayLength="5000000"/>
</binding>
</wsHttpContextBinding>
</bindings>
</system.serviceModel>
</configuration>

Nous déclarons ici un service de nom WorkflowFormulaireLibrary.WorkflowTraitementFormulaire (correspondant au nom du Workflow instancié). Nous définissons une adresse de base utilisée par les endpoints du service. Ici il ne sera créé qu'un seul endpoint dont l'adresse (UploadService) est relative à l'adresse de base définie précédemment. Nous utilisons le nouveau binding wsHttpContextBinding introduit avec le Framework 3.5. Enfin le contrat correspond bien sûr au contrat WCF que nous avons créé dans la librairie FileUploadServiceLibrary et utilisé par le Workflow. Un point important est la configuration associée au binding. Celle-ci possède plusieurs paramètres qui influencent la façon dont le service traite les messages. La propriété messageEncoding indique que l'encodage des données binaires utilisera l'encodage MTOM qui permet d'envoyer des données binaires plus efficacement qu'avec un message SOAP. La propriété maxReceivedMessageSize définite la taille maximale d'un message reçu par le service. Enfin la propriété maxArrayLength limite la longueur des chaînes et tableaux contenant des tableaux d'octets. Dans tous les cas nous fixons la limite à 5Mo.

V - Le client

Côté client, il ne s'agit que d'un simple appel à un service WCF. La notion de Workflow n'existe pas. Il nous faut ajouter les références aux assemblies suivantes System.ServiceModel, System.WorkflowServices et FileUploadServiceLibrary (contient le contrat de service).

Code du client

static void Main(string[] args)
{
try
{
Console.WriteLine("Appuyez sur ENTREE quand le service est pret");
Console.ReadLine();

ChannelFactory<FileUploadServiceLibrary.IUploadService> factory =
new ChannelFactory<FileUploadServiceLibrary.IUploadService>("ConfigClientUploadService");

FileUploadServiceLibrary.IUploadService channel = factory.CreateChannel();

string filePath = Path.Combine(System.Environment.CurrentDirectory, "formulaire.docx");
byte[] filebytes = System.IO.File.ReadAllBytes(filePath);

Console.WriteLine("Envoi du fichier");

ResultatTraitement resultat = channel.UploadFile(filebytes);

Console.WriteLine(String.Format("Formulaire accepté: {0}", resultat.Accepte.ToString()));
Console.WriteLine(String.Format("Numéro de dossier: {0}", resultat.NumDossier.ToString()));
Console.WriteLine(String.Format("Commentaire: {0}", resultat.Commentaire));

Console.WriteLine("Press any key to continue...");
Console.ReadKey();
}
catch (Exception ex)
{
Console.Write(String.Format("Erreur: {0}", ex.Message));
Console.ReadLine();
}
}

Le code est tout à fait classique : nous instancions une factory en lui passant en paramètre le nom de la configuration (se trouvant dans le fichier de configuration) à utiliser. La méthode CreateChannel permet de créer un canal de communication typé (IUploadService).

Nous copions ensuite le contenu du formulaire dans un tableau d'octets que nous passons en paramètre à la fonction UploadFile.

Jetons un coup d'œil au fichier de configuration :

Configuration du client

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.serviceModel>
<client>
<endpoint bindingConfiguration="BindingConfig"
address="http://localhost:8731/Design_Time_Addresses/FileUploadServiceLibrary/UploadService"
binding="wsHttpContextBinding"
contract="FileUploadServiceLibrary.IUploadService"
name="ConfigClientUploadService">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
</client>
<bindings>
<wsHttpContextBinding>
<!-- Taille fixé à 5 MB -->
<binding name="BindingConfig" messageEncoding="Mtom" maxReceivedMessageSize="5000000"/>
</wsHttpContextBinding>
</bindings>
</system.serviceModel>
</configuration>

Une fois lancé le client affiche le résultat suivant :

Le traitement du formulaire s'est apparemment bien passé !

Côté hôte nous obtenons le résultat suivant :

Un dossier formulaire_10-07-2008__22_31_36 a été créé contenant le formulaire Open XML reçu, le custom XML extrait et la photo désérialisée.

Conclusion

Cet article à été l'occasion d'aborder une utilisation combinée du format Open XML via sa prise en charge des schémas métier et du SDK Open XML, de Windows Workflow Foundation via la création d'un Workflow et d'activités personnalisées et enfin de WCF et son intégration avec Workflow Foundation via l'activité ReceiveActivity.

Sources

Téléchargez les sources de la solution donnée en exemple.

Liens

Pour en savoir plus sur les Custom XML parts

Le blog de Julien Chable

http://www.codeplex.com/dbe

Word 2007 Content Control Toolkit

Documentation SDK OpenXML 1.0

Windows Workflow Foundation sur MSDN

Remerciements

J'adresse ici tous mes remerciements à l'équipe de rédaction de « developpez.com » pour le temps qu'ils ont bien voulu passer à la correction et à l'amélioration de cet article.

Contact

Si vous constatez une erreur dans le tutoriel, dans les sources, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par le forum.