Gestion d'un formulaire OpenXML avec Windows Workflow Foundation
Date de publication : 04/09/2008 , Date de mise à jour : 04/09/2008
Par
Florian Casabianca (mon site)
(Aucun commentaire)
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.
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étiers 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 apparaître 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 (bi-directionnel) 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 entres 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 noud du document XML à un Content Control il suffit simplement d'effectuer un drag n' drop du noud sur le Content Control en question (du panneau de gauche). Et c'est tout ! A noter que pour sélectionner un noud 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 connaître 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. Etant 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 à coté:
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 apparaît demandant le contrat de service à importer. Dans le noud 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 disparue.
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 |
private byte[] _fileBytes;
public byte[] FileBytes
{
get
{
return _fileBytes;
}
set
{
_fileBytes = value;
}
}
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 apparaît. 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 appell un service coté 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
{
if (!Directory.Exists(this.FolderPathToSaveDocs))
Directory.CreateDirectory(this.FolderPathToSaveDocs);
Console.WriteLine("Enregistrement du fichier sous: {0}", this.FormulairePath);
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 |
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;
}
}
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:
|
private string _schemaPath = Path.Combine(System.Environment.CurrentDirectory, "Animal.xsd");
public string SchemaPath
{
get
{
return _schemaPath;
}
}
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é.
-
Validation
XDocument
Activity
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 donc une manipulation plus facile de documents OpenXML. A 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)
{
XDocument customXmlPropertiesPart = XDocument.Load(XmlReader.Create(customXmlPart.CustomXmlPropertiesPart.GetStream()));
string uri = customXmlPropertiesPart.Descendants(ds + "schemaRef").First().Attribute(ds + "uri").Value;
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 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éussit 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>
</summary>
<param name="executionContext"></param>
<returns></returns>
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
Console.WriteLine("Validation du custom XML");
if (XDocumentToValidate != null)
{
try
{
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))
{
XmlSchema animauxSchema = XmlSchema.Read(reader, null);
reader.Close();
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));
[
|