Cet article a pour but de faire un tour d'horizon des possibilités offertes par Linq pour lire et écrire des données xml.
Il existe plusieurs méthodes pour sérialiser / déssérialiser des données xml.
Par habitude, et parce qu'un jour j'ai pu faire ce que je n'arrivais autrement (ça ne veut pas dire que cela n'était pas réalisable, juste que je n'ai pas trouvé), LinqToXml a ma préférence.
Pour rappel Linq est un système de requêtes permettant de manipuler tout et n'importe quoi.
On distingue deux écritures que l'on peut utiliser plus ou moins indifféremment.
//code C# resultats = donnees.Where(d => d.Champ1 == ValeurATrouver).Select(d=> new ResultatUnitaire(d.Champ2, d.champ4));
'code VB resultats = donnees.Where(Function(d) d.Champ1 = ValeurATrouver).Select(Function(d) New ResultatUnitaire(d.Champ2, d.champ4))
//code C# resultats = (from d in donnees where d.Champ1 == ValeurATrouver select new ResultatUnitaire(d.Champ2, d.champ4));
'code VB resultats = ( From d In donnees Where d.Champ1 = ValeurATrouver Select New ResultatUnitaire(d.Champ2, d.champ4))
C'est cette deuxième qui est généralement utilisée pour le xml.
Par défaut Linq retourne une collection IEnumerable<T> (T étant le type des objets retournés). Cette collection est apparue avec Linq et permet l'évaluation différée de la requête.
Ceci ne présentant aucun intérêt pour ce tutoriel, les resultats seront castés en collection plus "classique" List<T> ou tableau.
Besoin de plus d'infos sur Linq avant d'aller plus loin? Ce tuto est une bonne base.
Afin d'avoir un exemple concret, nous allons réaliser une liste de contacts.
Note 1, les exemples on été écrit en C#, puis traduits automatiquement en VB, il peut donc y avoir des erreurs qui seraient passées inaperçues dans ce langage.
Note 2, VB n'aime pas les commentaires au milieu des requêtes, pensez à les enlever pour vos tests.
Commençons par écrire la classe qui recevra les données de chaque contact.
//code C# using System; namespace Tuto_LinqToXml { public class Personne { public string Nom { get; set; } public string Prenom { get; set; } public DateTime Naissance { get; set; } } }
'code VB Imports System Namespace Tuto_LinqToXml Public Class Personne Public Property Nom() As String Public Property Prenom() As String Public Property Naissance() As DateTime End Class
Il n'y a pas besoin de mettre d'attribut définissant quelle propriété sera sérialisée, ni comment (membre ou attribut).
Pour l'instant cette classe est la plus simple qui soit, 3 propriétés et pas de constructeur (donc constructeur par défaut, sans paramètre).
Il existe de nombreuses façon d'instancier un objet, en voici quelques unes, dont certaines seront utilisées par la suite:
//code C# Personne personne1 = new Personne(); personne1.Nom = "Sort"; personne1.Prenom = "Jean"; personne1.Naissance = DateTime.Parse("01/01/2015");
'code VB Dim personne1 As New Personne() personne1.Nom = "Sort" personne1.Prenom = "Jean" personne1.Naissance = Date.Parse("01/01/2015")
C'est la méthode "de base" pour tout objet, mais elle n'est pas utilisable avec Linq.
La contrainte, avec l'écriture spaghetti, est que chaque clause doit s'écrire sur "une ligne".
Il faut donc écrire un constructeur pour la classe Personne.
C'est lui qui affecte les propriétés, il effectue même le parsage de la date de naissance.
//code C# using System; namespace Tuto_LinqToXml { public class Personne { public Personne(string Nom, string Prenom, string Naissance) { this.Nom = Nom; this.Prenom = Prenom; this.Naissance = DateTime.Parse(Naissance); } public string Nom { get; set; } public string Prenom { get; set; } public DateTime Naissance { get; set; } } }
'code VB Imports System Namespace Tuto_LinqToXml Public Class Personne Public Sub New(ByVal Nom As String, ByVal Prenom As String, ByVal Naissance As String) Me.Nom = Nom Me.Prenom = Prenom Me.Naissance = Date.Parse(Naissance) End Sub Public Property Nom() As String Public Property Prenom() As String Public Property Naissance() As Date End Class End Namespace
Pour instancier Personne, il suffit maintenant d'une ligne
//code C# Personne personne2 = new Personne("Di", "Alain", "02/02/2015");
'code VB Dim personne2 As New Personne("Di", "Alain", "02/02/2015")
Cette méthode est un "raccourci" de la première, le constructeur sans paramètre est appelé et les propriétés renseignées entre les accolades sont affectées.
Dans notre cas, Personne n'a plus de constructeur sans paramètre, il faut donc un ajouter un.
//code C# using System; namespace Tuto_LinqToXml { public class Personne { public Personne() { } public Personne(string Nom, string Prenom, string Naissance) { this.Nom = Nom; this.Prenom = Prenom; this.Naissance = DateTime.Parse(Naissance); } public string Nom { get; set; } public string Prenom { get; set; } public DateTime Naissance { get; set; } } }
'code VB Imports System Namespace Tuto_LinqToXml Public Class Personne Public Sub New() End Sub Public Sub New(ByVal Nom As String, ByVal Prenom As String, ByVal Naissance As String) Me.Nom = Nom Me.Prenom = Prenom Me.Naissance = Date.Parse(Naissance) End Sub Public Property Nom() As String Public Property Prenom() As String Public Property Naissance() As Date End Class End Namespace
L'instanciation sera alors
//code C# Personne personne3 = new Personne { Naissance = DateTime.Parse("03/03/2015"), Prenom = "Mélanie" };
'code VB Dim personne3 As Personne = New Personne With {.Naissance = Date.Parse("03/03/2015"), .Prenom = "Mélanie"}
On constate que l'on peut affecter les propriétés que l'on veut, et dans l'ordre que l'on souhaite, ce point peut être utile quand l'affectation d'une propriété dépend de la valeur d'une autre.
Une méthode de classe est une méthode qui peut être appelée sans que la classe soit instanciée. On peut écrire une méthode de classe qui retourne une instance.
C'est le cas de DateTime.Parse(). A l'intérieur, on appelle un constructeur et on affecte les propriétés.
Quel est l'intérêt par rapport à un constructeur?
Imaginons que l'on souhaite instancier Personne, avec une seule propriété qui serait au choix le nom, le prénom ou la date de naissance (en string).
On ne peut pas écrire deux constructeurs avec la même signature.
//code C# public Personne(string Nom) { } public Personne(string Prenom) { } public Personne(string Naissance) { }
'code VB Public Sub New(Nom As String) End Sub Public Sub New(Prenom As String) End Sub Public Sub New(Naissance As String) End Sub
Ca ne compile pas, dés la 2eme ligne, on a une erreur
Erreur 1 Le type 'Tuto_LinqToXml.Personne' définit déjà un membre appelé 'Personne' avec les mêmes types de paramètres
La méthode de classe permet de contourner ce problème.
//code C# public static Personne FromPrenom(string Prenom) { return new Personne { Prenom = Prenom }; } public static Personne FromNaissance(string Naissance) { return new Personne { Naissance = DateTime.Parse(Naissance) }; }
'code VB Public Shared Function FromPrenom(ByVal Prenom As String) As Personne Return New Personne With {.Prenom = Prenom} End Function Public Shared Function FromNaissance(ByVal Naissance As String) As Personne Return New Personne With {.Naissance = Date.Parse(Naissance)} End Function
Que l'on instanciera comme ceci:
//code C# Personne personne4 = Personne.FromPrenom("Toto"); Personne personne5 = Personne.FromNaissance("04/04/2015");
'code VB Dim personne4 As Personne = Personne.FromPrenom("Toto") Dim personne5 As Personne = Personne.FromNaissance("04/04/2015")
Le fichier doit contenir une balise "Root", contenant une balise "Contacts", elle-même listant nos contact dans une balise "Contact" possédant le membre Nom, avec l'attribut Prenom et le membre DateNaissance.
On constate que les noms des balises ne sont pas forcément identiques avec les propriétés ou nom de la classe, cela ne pose aucune difficulté.
Nous disposons d'une liste de Personne, nous pouvons donc maintenant exporter les données dans le fichier xml, en suivant le schéma définit au-dessus.
//code C# void Serialise(List<Personne> MesPersonnes) { XDocument xDoc = new XDocument( new XElement("Root",//la balise Root new XComment("Ceci est mon répertoire de contacts"),//ça ne fait pas de mal new XElement("Contacts",//La liste de contacts (from p in MesPersonnes//la requête qui pour chaque item de la liste va écrire ce que l'on souhaite select new XElement("Contact", new XElement("Nom",//l'élément Nom new XAttribute("Prenom", p.Prenom),// avec l'attribut Prenom p.Nom ), new XElement("DateNaissance",p.Naissance)//et l'élément DateNaissance ) ) ) ) ); xDoc.Save("exemple.xml"); }
'code VB Private Sub Serialise(ByVal MesPersonnes As List(Of Personne)) Dim xDoc As New XDocument(New XElement("Root",' la balise Root New XComment("Ceci est mon répertoire de contacts"),'ça ne fait pas de mal New XElement("Contacts", (' La liste de contacts From p In MesPersonnes' la requête qui pour chaque item de la liste va écrire ce que l'on souhaite Select New XElement("Contact", New XElement("Nom",' l'élément Nom New XAttribute("Prenom", p.Prenom)' avec l'attribut Prenom , p.Nom), New XElement("DateNaissance",p.Naissance) 'et l'élément DateNaissance ))))) xDoc.Save("exemple.xml") End Sub
Remarquez comme l'arborescence du fichier se "voit" au travers de l'indentation du code.
On peut évidement placer ce code ou l'on veut. Mais il me semble logique que l'objet sache se sérialiser lui-même.
Par contre, il n'est pas nécessaire d'avoir une instance pour effectuer la sérialisation, on place donc ce code dans une méthode de classe.
L'appel se fait comme ceci:
//code C# //initialisation d'une liste de personnes avec plusieurs méthodes Personne personne1 = new Personne(); personne1.Nom = "Sort"; personne1.Prenom = "Jean"; personne1.Naissance = DateTime.Parse("01/01/2015"); List<Personne> mesPersonnes = new List<Personne>(); mesPersonnes.Add(personne1); mesPersonnes.Add(new Personne("Di", "Alain", "02/02/2015")); mesPersonnes.Add(new Personne { Naissance = DateTime.Parse("03/03/2015"), Prenom = "Mélanie" }); mesPersonnes.Add(Personne.FromPrenom("Toto")); //sérialisation de la liste Personne.Serialise(mesPersonnes);
'code VB 'initialisation d'une liste de personnes avec plusieurs méthodes Dim personne1 As New Personne() personne1.Nom = "Sort" personne1.Prenom = "Jean" personne1.Naissance = Date.Parse("01/01/2015") Dim mesPersonnes As New List(Of Personne)() mesPersonnes.Add(personne1) mesPersonnes.Add(New Personne("Di", "Alain", "02/02/2015")) mesPersonnes.Add(New Personne With {.Naissance = Date.Parse("03/03/2015"), .Prenom = "Mélanie"}) mesPersonnes.Add(Personne.FromPrenom("Toto")) 'sérialisation de la liste Personne.Serialise(mesPersonnes)
Et voila le résultat
<?xml version="1.0" encoding="utf-8"?> <Root> <!--Ceci est mon répertoire de contacts--> <Contacts> <Contact> <Nom Prenom="Jean">Sort</Nom> <DateNaissance>2015-01-01T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Alain">Di</Nom> <DateNaissance>2015-02-02T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Mélanie" /> <DateNaissance>2015-03-03T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Toto" /> <DateNaissance>0001-01-01T00:00:00</DateNaissance> </Contact> </Contacts> </Root> </xml>
A noter que pour cet exemple, je n'ai pas mis d'instance avec juste une date de naissance, car en l'état Nom et Prenom null ça ne fonctionne pas.
Même principe, une méthode de classe qui grâce à une requête retourne une liste de Personne.
//code C# public static List<Personne> Desserialisation() { XDocument xDox = XDocument.Load("exemple.xml"); return (from c in xDox.Descendants("Contact")//ici on va directement à la balise qui contient nos données, Descendants("Contact") retourne une collection du contenu de chaque balise "Contact" select new Personne(c.Element("Nom").Value, c.Element("Nom").Attribute("Prenom").Value, c.Element("DateNaissance").Value)//on instancie sur une ligne ).ToList();//Cast en List ici }
'code VB Public Shared Function Desserialisation() As List(Of Personne) Dim xDox As XDocument = XDocument.Load("exemple.xml") Return ( From c In xDox.Descendants("Contact")' ici on va directement à la balise qui contient nos données, Descendants("Contact") retourne une collection du contenu de chaque balise "Contact" Select New Personne(c.Element("Nom").Value, c.Element("Nom").Attribute("Prenom").Value, c.Element("DateNaissance").Value) ' ici on instancie sur une ligne ).ToList() 'Cast en List End Function
Que l'on appelle comme çà:
//code C# mesPersonnes = Personne.Desserialisation();
'code VB mesPersonnes = Personne.Desserialisation()
Un reproche souvent fait à LinqToXml est que c'est verbeux, on le voit bien (plus pour VB que C# d'ailleurs) , mais, du coup on peu gérer tout ce que l'on veut comme on veut.
Soit l'enum suivant
//code C# public enum TypeContact { Famille, Amis, Travail, Autre }
'code VB Public Enum TypeContact Famille Amis Travail Autre End Enum Private
</code>
La propriété associée dans Personne
//code C# public TypeContact Type { get; set; }
'code VB Public Property Type() As TypeContact
Pour la sérialisation, rien de compliqué, on ajoute un élément (ou un attribut) à notre requête.
//code C# new XElement("TypeDeContact",p.Type)
'code VB XElement("TypeDeContact",p.Type)
<?xml version="1.0" encoding="utf-8"?> <Root> <!--Ceci est mon répertoire de contacts--> <Contacts> <Contact> <Nom Prenom="Jean">Sort</Nom> <DateNaissance>2015-01-01T00:00:00</DateNaissance> <TypeDeContact>Famille</TypeDeContact> </Contact> <Contact> <Nom Prenom="Alain">Di</Nom> <DateNaissance>2015-02-02T00:00:00</DateNaissance> <TypeDeContact>Amis</TypeDeContact> </Contact> <Contact> <Nom Prenom="Mélanie">Pas</Nom> <DateNaissance>2015-03-03T00:00:00</DateNaissance> <TypeDeContact>Travail</TypeDeContact> </Contact> <Contact> <Nom Prenom="Toto">Pas</Nom> <DateNaissance>0001-01-01T00:00:00</DateNaissance> <TypeDeContact>Famille</TypeDeContact> </Contact> <Contact> <Nom Prenom="Jean">Michmuche</Nom> <DateNaissance>2015-05-05T00:00:00</DateNaissance> <TypeDeContact>Amis</TypeDeContact> </Contact> <Contact> <Nom Prenom="On lappel">Pas</Nom> <DateNaissance>2015-04-04T00:00:00</DateNaissance> <TypeDeContact>Travail</TypeDeContact> </Contact> </Contacts> </Root>
Pour la dessérialisation, dans notre constructeur, il faut parser le texte, puis le caster, pour obtenir la bonne valeur d'énumération.
//code C# Type = (TypeContact)Enum.Parse(typeof(TypeContact), Element.Element("TypeDeContact").Value);
'code VB Type = CType(System.Enum.Parse(GetType(TypeContact), Element.Element("TypeDeContact").Value), TypeContact)
On a vu plus haut que la sérialisation de valeurs nulles peuvent poser des soucis.
Il faut donc les gérer.
Si on affecte une valeur par défaut à la sérialisation, celle-ci sera renseignée lors de la dessérialisation, au final il n'y aura plus de valeur nulle.
Cette méthode a l'avantage de ne nécessiter du code que dans la partie sérialisation.
Il nous faut tester la variable et retourner la valeur par défaut si nécessaire.
Par contre, nous somme toujours contraint à ce que l'instruction tienne sur une ligne.
En C#, le mot clé if, n'est pas reconnu dans une reqête Linq, on utilise donc l'opérateur unuaire. Par contre, en VB cet opérateur n'existe pas, if est donc accepeté.
//code C# XDocument xDoc = new XDocument( new XElement("Root", new XComment("Ceci est mon répertoire de contacts"), new XElement("Contacts", (from p in MesPersonnes select new XElement("Contact", new XElement("Nom", new XAttribute("Prenom", string.IsNullOrWhiteSpace(p.Prenom) ? "On lappel" : p.Prenom),//Valeur par défaut "On lappel" string.IsNullOrWhiteSpace(p.Nom)? "Pas" : p.Nom//Valeur par défaut "Pas" ), new XElement("DateNaissance",p.Naissance) ) ) ) ) );
'code VB Dim xDoc As New XDocument(New XElement("Root", New XComment("Ceci est mon répertoire de contacts"), New XElement("Contacts", ( From p In MesPersonnes Select New XElement("Contact", New XElement("Nom", New XAttribute("Prenom",If(String.IsNullOrWhiteSpace(p.Prenom), "On lappel", 'Valeur par défaut "On lappel" p.Prenom)), If(String.IsNullOrWhiteSpace(p.Nom), "Pas",p.Nom)),'Valeur par défaut "Pas" New XElement("DateNaissance",p.Naissance))))))
Résultat:
<?xml version="1.0" encoding="utf-8"?> <Root> <!--Ceci est mon répertoire de contacts--> <Contacts> <Contact> <Nom Prenom="Jean">Sort</Nom> <DateNaissance>2015-01-01T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Alain">Di</Nom> <DateNaissance>2015-02-02T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Mélanie">Pas</Nom> <DateNaissance>2015-03-03T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Toto">Pas</Nom> <DateNaissance>0001-01-01T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="Jean">Michmuche</Nom> <DateNaissance>2015-05-05T00:00:00</DateNaissance> </Contact> <Contact> <Nom Prenom="On lappel">Pas</Nom> <DateNaissance>2015-04-04T00:00:00</DateNaissance> </Contact> </Contacts> </Root>
Cependant on modifie les données, ce qui peut ne pas être souhaitable.
Souvent, quand une valeur est nulle, la balise n'est tout simplement pas présente dans le xml.
Ceci nécessite une gestion à la lecture et à l'écriture.
Commençons par la lecture.
Pour chaque enregistrement, il va falloir tester la présence des éléments et de leurs attributs, et tout ça doit tenir sur une ligne dans la requête, je vous le rappelle.
Parfois, pour y parvenir, on peut s'arracher les cheveux.
Une solution est d'utiliser un constructeur ou une méthode de classe qui prend l'élément complet en paramètre. A l'intérieur on a tout loisir de d'écrire autant de code et de lignes que nécessaire.
//code C# public Personne(XElement Element) { XElement nom = Element.Element("Nom"); if (nom != null)//On teste que la balise existe { if (nom.Attribute("Prenom") != null) Prenom = nom.Attribute("Prenom").Value;//on teste que l'attribut existe Nom = nom.Value;//string étant nullable peut importe que value existe ou pas } if (Element.Elements("DateNaissance").Count() == 1) Naissance = DateTime.Parse(Element.Element("DateNaissance").Value); }
'code VB Public Sub New(ByVal Element As XElement) Dim nom As XElement = Element.Element("Nom") If nom IsNot Nothing Then On teste que la balise existe If nom.Attribute("Prenom") IsNot Nothing Then 'on teste que l'attribut existe Prenom = nom.Attribute("Prenom").Value End If Nom = nom.Value 'string étant nullable peut importe que value existe ou pas End If If Element.Elements("DateNaissance").Count() = 1 Then Naissance = Date.Parse(Element.Element("DateNaissance").Value) End If End Sub
Le requête devient.
//code C# return (from c in xDox.Descendants("Contact") select new Personne(c)//on envoie l'élément complet ).ToList();
'code VB Return ( From c In xDox.Descendants("Contact") Select New Personne(c)).ToList() 'on envoie l'élément complet
En aparté, le déboguage.
Quand une requête bogue, c'est dans son ensemble, on ne sait pas quelle ligne ne lui convient pas (en tout cas avec Visual Studio 2013 et antérieurs).
Le fait de mettre du code "en dehors" de la requête, comme ci-dessus le constructeur, permet d'y mettre des points d'arrêt et ainsi pouvoir déboguer.
On imagine que si on a de nombreux champ, on va répéter autant de fois la séquence "la balise existe-t-elle? oui alors je prends sa valeur".
Et un programme ou le même code se répète est un mauvais programme, conclusion factorisons! OK, mais ou?
Si je fais une méthode dans Personne, il m'en faudra une aussi dans Entreprise, et dans Produit.
Il faut donc factoriser plus haut.
Un bon moyen est de faire une extension de XElement et XAttribut.
//Code C# using System.Xml.Linq; namespace System.Xml.Linq//note je mets cette classe dans le namespace de LinqToXml pour pouvoir la référencer à tous mes projets utilisant cette technologie sans avoir à adapter le namespace { public static class Extensions { /// <summary> /// Méthode d'extension de la classe XElement qui retourne la d'un attribut s'il existe /// </summary> /// <param name="Attribut">Nom de l'attribut recherché</param> public static string AttributNullable(this XElement ElementPere, string Attribut) { if (ElementPere.Attribute(Attribut) == null) return null; return ElementPere.Attribute(Attribut).Value; } } }
'code VB Imports System.Xml.Linq Namespace System.Xml.Linq 'note je mets cette classe dans le namespace de LinqToXml pour pouvoir la référencer à tous mes projets utilisant cette technologie sans avoir à adapter le namespace Public Module Extensions ''' <summary> ''' Méthode d'extension de la classe XElement qui retourne la d'un attribut s'il existe ''' </summary> ''' <param name="Attribut">Nom de l'attribut recherché</param> <System.Runtime.CompilerServices.Extension> _ Public Function AttributNullable(ByVal ElementPere As XElement, ByVal Attribut As String) As String If ElementPere.Attribute(Attribut) Is Nothing Then Return Nothing End If Return ElementPere.Attribute(Attribut).Value End Function End Module End Namespace
Dans notre constructeur, on adapte le code de cette façon
//code C# public Personne(XElement Element) { XElement nom = Element.Element("Nom"); if (nom != null) { Prenom = nom.AttributNullable("Prenom"); Nom = nom.Value; } string naissance = Element.AttributNullable("DateNaissance"); if (naissance != null) Naissance = DateTime.Parse(naissance); }
'code VB Public Sub New(ByVal Element As XElement) Dim nom As XElement = Element.Element("Nom") If nom IsNot Nothing Then Prenom = nom.AttributNullable("Prenom") Nom = nom.Value End If Dim naissance As String = Element.AttributNullable("DateNaissance") If naissance IsNot Nothing Then Naissance = Date.Parse(naissance) End If End Sub
Pour Prenom, qui est un string, c'est super, ça tient sur une ligne. Mais pour la date de naissance, il faut passer par une variable intermédiaire, la tester et ensuite la convertir....
Peut on faire mieux? Oui....
//code C# using System; using System.Xml.Linq; namespace System.Xml.Linq { public static class Extensions { /// <summary> /// Méthode d'extension de la classe XElement qui retourne la d'un attribut s'il existe /// </summary> /// <param name="Attribut">Nom de l'attribut recherché</param> public static string AttributNullable(this XElement ElementPere, string Attribut) { string valeur; if (!ElementPere.AttributExits(Attribut, out valeur)) return null; return valeur; } /// <summary> /// Teste si un attribut portant le nom en paramètre existant dans l'élément en cours, et retourne sa valeur le cas échéant /// </summary> /// <param name="ElementPere"></param> /// <param name="Attribut"></param> /// <returns></returns> public static bool AttributExits(this XElement ElementPere, string Attribut, out string Valeur) { bool test = ElementPere.Attribute(Attribut) != null; Valeur = test ? ElementPere.Attribute(Attribut).Value : null; return test; } /// <summary> /// Teste si un élément portant le nom en paramètre existant dans l'élément en cours, et retourne sa valeur le cas échéant /// </summary> public static bool ElementExits(this XElement ElementPere, string Attribut, out string Valeur) { bool test = ElementPere.Element(Attribut) != null; Valeur = test ? ElementPere.Element(Attribut).Value : null; return test; } /// <summary> /// Affecte une date à Variable si l'attribut et la valeur existent /// </summary> public static void AttributNullable(this XElement ElementPere, string Attribut,ref DateTime Variable) { string valeur; if (!ElementPere.ElementExits(Attribut, out valeur)) return; if (valeur != null) Variable = DateTime.Parse(valeur); } } }
'code VB Imports System Imports System.Xml.Linq Namespace System.Xml.Linq Public Module Extensions ''' <summary> ''' Méthode d'extension de la classe XElement qui retourne la d'un attribut s'il existe ''' </summary> ''' <param name="Attribut">Nom de l'attribut recherché</param> <System.Runtime.CompilerServices.Extension> _ Public Function AttributNullable(ByVal ElementPere As XElement, ByVal Attribut As String) As String Dim valeur As String If Not ElementPere.AttributExits(Attribut, valeur) Then Return Nothing End If Return valeur End Function ''' <summary> ''' Teste si un attribut portant le nom en paramètre existant dans l'élément en cours, et retourne sa valeur le cas échéant ''' </summary> ''' <param name="ElementPere"></param> ''' <param name="Attribut"></param> ''' <returns></returns> <System.Runtime.CompilerServices.Extension> _ Public Function AttributExits(ByVal ElementPere As XElement, ByVal Attribut As String, <System.Runtime.InteropServices.Out()> ByRef Valeur As String) As Boolean Dim test As Boolean = ElementPere.Attribute(Attribut) IsNot Nothing Valeur = If(test, ElementPere.Attribute(Attribut).Value, Nothing) Return test End Function ''' <summary> ''' Teste si un élément portant le nom en paramètre existant dans l'élément en cours, et retourne sa valeur le cas échéant ''' </summary> <System.Runtime.CompilerServices.Extension> _ Public Function ElementExits(ByVal ElementPere As XElement, ByVal Attribut As String, <System.Runtime.InteropServices.Out()> ByRef Valeur As String) As Boolean Dim test As Boolean = ElementPere.Element(Attribut) IsNot Nothing Valeur = If(test, ElementPere.Element(Attribut).Value, Nothing) Return test End Function ''' <summary> ''' Affecte une date à Variable si l'attribut et la valeur existent ''' </summary> <System.Runtime.CompilerServices.Extension> _ Public Sub AttributNullable(ByVal ElementPere As XElement, ByVal Attribut As String, ByRef Variable As Date) Dim valeur As String If Not ElementPere.ElementExits(Attribut, valeur) Then Return End If If valeur IsNot Nothing Then Variable = Date.Parse(valeur) End If End Sub End Module End Namespace
Ce code affectera la "Variable" en paramètre si la balise et la valeur existe, <gras>mais...<gras>
avec les types "valeur" (comme ici DateTime, int, double, les enum, etc...) pour que cela fonctionne, il faut les passer avec le mot clé ref, ou est le problème me direz-vous?
//code C# Element.AttributNullable("DateNaissance", ref Naissance);
'code VB Element.AttributNullable("DateNaissance", Naissance)
Cette ligne ne compile pas: <block>Erreur 1 Une propriété, un indexeur ou l'accès au membre dynamique ne peut pas être passé en tant que paramètre de sortie (out) ni de référence (ref) <block>
Il faut donc passer *soit par une variable intermédiaire.
//code C# DateTime date = new DateTime(); Element.AttributNullable("DateNaissance", ref date); Naissance = date;
'code VB Dim madate As New Date() Element.AttributNullable("DateNaissance", madate) Naissance = madate
//code C# private DateTime naissance; public DateTime Naissance { get { return naissance; } set { naissance = value; } } //Et dans le constructeur Element.AttributNullable("DateNaissance", ref naissance);
'code VB Private pnaissance As Date Public Property Naissance() As Date Get Return pnaissance End Get Set(ByVal value As Date) pnaissance = value End Set End Property 'Et dans le constructeur Element.AttributNullable("DateNaissance", pnaissance)
La seconde façon a ma préférence, si part la suite on a besoin d'implémenter iPropertyChanged, ou gérer des valeurs correctes (masque, intervalles, etc..) la propriété est déjà écrite avec une variable interne.
On peut aussi initialiser nos propriétés avec une valeur par défaut. Dans ce cas c'est beaucoup plus simple.
//code C# /// <summary> /// Retourne une date si elle existe, sinon la valeur par défaut du type. /// </summary> public static DateTime ElementNullable(this XElement ElementPere, string Attribut) { string valeur; if (ElementPere.ElementExits(Attribut, out valeur) && valeur != null) return DateTime.Parse(valeur); return new DateTime(); }
'code VB ''' <summary> ''' Retourne une date si elle existe, sinon la valeur par défaut du type. ''' </summary> <System.Runtime.CompilerServices.Extension> _ Public Shared Function ElementNullable(ByVal ElementPere As XElement, ByVal Attribut As String) As Date Dim valeur As String If ElementPere.ElementExits(Attribut, valeur) AndAlso valeur IsNot Nothing Then Return Date.Parse(valeur) End If Return New Date() End Function
et l'affectation
//code C# Naissance = Element.ElementNullable("DateNaissance");
'code VB Naissance = Element.ElementNullable("DateNaissance")
N'empêche, il faut encore faire une extension pour chaque type de base, et pour un élément et pour un attribut.
On peut faire encore mieux (ou au moins plus factorisé!) avec une méthode anonyme:
//Code # /// <summary> /// Test la présence d'une balise (élément d'abord, puis dans la négative attribut) dans l'élément en cours, si elle existe et posséde une valeur, retourne cette valeur, sinon la valeur par défaut du type /// </summary> /// <typeparam name="T">Type retour attendu</typeparam> /// <param name="Balise">Nom de la balise recherchée</param> /// <returns></returns> public static T BaliseNullable<T>(this XElement ElementPere, string Balise) { string valeur; if (! ElementPere.ElementExits(Balise, out valeur)) ElementPere.AttributExits(Balise, out valeur);//on cherchera l'attibut si l'élement n'existe pas if (typeof(T).IsEnum) { if (string.IsNullOrWhiteSpace(valeur)) return (T)(object)0; return (T)Enum.Parse(typeof(T), valeur); } else { if (string.IsNullOrWhiteSpace(valeur)) return default(T);//si la valeur est nulle ou vide on retourne la valeur par defaut return (T)Convert.ChangeType(valeur, typeof(T)); } }
'Code VB ''' <summary> ''' Test la présence d'une balise (élément d'abord, puis dans la négative attribut) dans l'élément en cours, si elle existe et posséde une valeur, retourne cette valeur, sinon la valeur par défaut du type ''' </summary> ''' <typeparam name="T">Type retour attendu</typeparam> ''' <param name="Balise">Nom de la balise recherchée</param> ''' <returns></returns> <System.Runtime.CompilerServices.Extension> _ Public Function BaliseNullable(Of T)(ByVal ElementPere As XElement, ByVal Balise As String) As T Dim valeur As String If Not ElementPere.ElementExits(Balise, valeur) Then ElementPere.AttributExits(Balise, valeur) 'on cherchera l'attibut si l'élement n'existe pas End If If GetType(T).IsEnum Then If String.IsNullOrWhiteSpace(valeur) Then Return CType(CObj(0), T) End If Return CType(System.Enum.Parse(GetType(T), valeur), T) Else If String.IsNullOrWhiteSpace(valeur) Then 'si la valeur est nulle ou vide on retourne la valeur par defaut Return Nothing End If Return CType(Convert.ChangeType(valeur, GetType(T)), T) End If End Function
Et dans le constructeur de Personne
//Code C# if (Element.ElementExits("Nom")) { Nom = Element.BaliseNullable<string>("Nom"); Prenom = Element.Element("Nom").BaliseNullable<string>("Prenom"); } Naissance = Element.BaliseNullable<DateTime>("DateNaissance"); Type = Element.BaliseNullable<TypeContact>("TypeDeContact");
'Code VB If Element.ElementExits("Nom") Then Nom = Element.BaliseNullable(Of String)("Nom") Prenom = Element.Element("Nom").BaliseNullable(Of String)("Prenom") End If Naissance = Element.BaliseNullable(Of Date)("DateNaissance") Type = Element.BaliseNullable(Of TypeContact)("TypeDeContact")
L'absence d'une balise est plus simple à gérer en écriture, il suffit de remplacer le XElement ou le XAttribut par null.
S'il s'agit d'un simple élément, comme la date de naissance, un test avec l'opérateur unuaire et suffisant.
//code C# p.Naissance != null ? new XElement("DateNaissance",p.Naissance):null,
'code VB If(p.Naissance <> New DateTime(), New XElement("DateNaissance",p.Naissance), Nothing),
Dans le cas d'un élément avec attribut(s), tout mettre sur une ligne est faisable, mais cela risque de devenir vite illisible.
Faire une méthode à part est plus judicieux.
//code C# private XElement BaliseNom() { if (string.IsNullOrWhiteSpace(Nom) && string.IsNullOrWhiteSpace(Prenom)) return null; //Retourne l'élément Nom avec un attribut Prénom s'il existe return new XElement("Nom", string.IsNullOrWhiteSpace(Prenom) ? null : new XAttribute("Prenom", Prenom), Nom); }
'code VB Private Function BaliseNom() As XElement If String.IsNullOrWhiteSpace(Nom) AndAlso String.IsNullOrWhiteSpace(Prenom) Then Return Nothing End If 'Retourne l'élément Nom avec un attribut Prénom s'il existe Return New XElement("Nom",If(String.IsNullOrWhiteSpace(Prenom), Nothing, New XAttribute("Prenom", Prenom)), Nom) End Function
Au final notre requête de sérialisation devient
//code C# (from p in MesPersonnes select new XElement("Contact", p.BaliseNom(), p.Naissance != null ? new XElement("DateNaissance",p.Naissance):null)
'code VB ( From p In MesPersonnes Select New XElement("Contact", p.BaliseNom(), If(p.Naissance <> New DateTime(), New XElement("DateNaissance",p.Naissance), Nothing))
Nous savons maintenant (des)sérialiser une collection d'objets dont les propriétés sont des types de base, on peut aussi y ajouter des types personnalisés.
Si nous ajoutons une classe Adresse, on pourrait coder la sérialisation et la dessérialisation dans Personne, mais il me parait plus logique de le faire dans Adresse.
Par contre, il faudra pouvoir appeler ces codes depuis Personne.
Pour la dessérialisation, on passera encore par le constructeur, et pour la sérialisation, nous allons écrire une propriété (pour changer de la méthode) retournant un XElement.
//code C# using System.Xml.Linq; namespace Tuto_LinqToXml { public class Adresse { public Adresse() { } public Adresse(XElement Element) { Numero = Element.ElementIntNullable("Numero"); Voie = Element.ElementStringNullable("Voie"); CodePostal = Element.ElementIntNullable("CodePostal"); Ville = Element.ElementStringNullable("Ville"); } public int Numero { get; set; } public string Voie { get; set; } public int CodePostal { get; set; } public string Ville { get; set; } public XElement Serialise { get { return new XElement("Adresse", new XElement("Numero", Numero), new XElement("Voie", Voie), new XElement("CodePostal", CodePostal), new XElement("Ville",Ville) ); } } } }
'code VB Imports System.Xml.Linq Namespace Tuto_LinqToXml Public Class Adresse Public Sub New() End Sub Public Sub New(ByVal Element As XElement) Numero = Element.ElementIntNullable("Numero") Voie = Element.ElementStringNullable("Voie") CodePostal = Element.ElementIntNullable("CodePostal") Ville = Element.ElementStringNullable("Ville") End Sub Public Property Numero() As Integer Public Property Voie() As String Public Property CodePostal() As Integer Public Property Ville() As String Public ReadOnly Property Serialise() As XElement Get Return New XElement("Adresse", New XElement("Numero", Numero), New XElement("Voie", Voie), New XElement("CodePostal", CodePostal), New XElement("Ville",Ville)) End Get End Property End Class End Namespace
Dans Personne, on ajoute une propriété Adresse, et on adapte la requête de sérialisation comme ceci:
//code C# (from p in MesPersonnes select new XElement("Contact", new XElement("Nom", p.BaliseNom(), p.Naissance != null ? new XElement("DateNaissance",p.Naissance):null, new XElement("TypeDeContact",p.Type), p.Adresse.Serialise//on utilise tout simplement la propriété Serialise, cela aurait été pareil avec une méthode, les parenthèses en plus )
'code VB (From p In MesPersonnes Select New XElement("Contact", New XElement("Nom", p.BaliseNom(), If(p.Naissance <> New DateTime(), New XElement("DateNaissance",p.Naissance), Nothing), New XElement("TypeDeContact",p.Type), p.Adresse.Serialise) 'on utilise tout simplement la propriété Serialise, cela aurait été pareil avec une méthode, les parenthèses en plus
Pour la dessérialisation, dans le constructeur de Personne, on ajoute
//code C# Adresse = new Adresse(Element.Element("Adresse"));
'code VB Adresse = New Adresse(Element.Element("Adresse"))
Il arrive souvent, que chaque objet contienne une liste, un tableau, ou autre, il se peut même que chaque objet de cette collection contienne lui-même une autre collection et ainsi de suite.
Remplaçons la propriété Adresse, par une liste d'adresses (maison, travail, résidence secondaire, etc...), ajoutons aussi un tableau Parents contenant l'identité du père et de la mère de notre contact.
Dans Adresse, on ajoute une méthode de classe qui appelle le constructeur déjà implémenté et retourne une liste d'adresses
//code C# public static List<Adresse> Desserialise(XElement Element) { return (from a in Element.Descendants("Adresse") select new Adresse(a)).ToList(); }
'code VB Public Shared Function Desserialise(ByVal Element As XElement) As List(Of Adresse) Return ( From a In Element.Descendants("Adresse") Select New Adresse(a)).ToList() End Function
Dans le constructeur de Personne, on va appeler cette méthode et créer le tableau de Parents
//code C# Parents = (from p in Element.Descendants("Parent") select p.Value ).ToArray(); Adresses = Adresse.Desserialise(Element);
'code VB Parents = ( From p In Element.Descendants("Parent") Select p.Value).ToArray() Adresses = Adresse.Desserialise(Element)
Enfin la requête de sérialisation devient
//code C# (from p in MesPersonnes select new XElement("Contact", new XElement("Nom", p.BaliseNom(), p.Naissance != null ? new XElement("DateNaissance",p.Naissance):null, new XElement("TypeDeContact",p.Type), new XElement("Parents", from parent in p.Parents select new XElement("Parent",parent) ), new XElement("Adresses",//note on peut aussi écrire une méthode de classe dans Adresse qui retournerait la balise "Adresses" entière from a in p.Adresses select a.Serialise ) ) )
'code VB (From p In MesPersonnes Select New XElement("Contact", New XElement("Nom", p.BaliseNom(), If(p.Naissance <> New DateTime(),New XElement("DateNaissance",p.Naissance), Nothing), New XElement("TypeDeContact",p.Type), New XElement("Parents", From parent In p.Parents Select New XElement("Parent",parent)), New XElement("Adresses",'note on peut aussi écrire une méthode de classe dans Adresse qui retournerait la balise "Adresses" entière From a In p.Adresses Select a.Serialise)))
Et avec un dictionnaire?
Si nous souhaitons savoir à quoi correspond telle ou telle adresse, on peut faire un dictionnaire, qui en clé prendra l'adresse et en valeur une énumération listant les lieux les plus communs.
//code C# public enum TypeAdresse { ResidencePrincipale, ResidenceSecondaire, Bureau, Autre }
'code VB Public Enum TypeAdresse ResidencePrincipale ResidenceSecondaire Bureau Autre End Enum
Dans le xml, TypeAdresse sera un attribut de la balise adresse.
Deux méthodes de classe:
//code C# public static Dictionary<Adresse, TypeAdresse> Desserialise(XElement Element) { string valeur; return Element.Descendants("Adresse").ToDictionary(a => new Adresse(a), a => a.AttributExits("Type", out valeur) ? (TypeAdresse)Enum.Parse(typeof(TypeAdresse),valeur) : TypeAdresse.Autre); } public static XElement Serialise(Dictionary<Adresse,TypeAdresse> Adresses) { return new XElement("Adresses", from a in Adresses select new XElement("Adresse", new XAttribute("Type", a.Value), new XElement("Numero", a.Key.Numero), new XElement("Voie", a.Key.Voie), new XElement("CodePostal", a.Key.CodePostal), new XElement("Ville", a.Key.Ville) ) ); }
'code VB Public Shared Function Desserialise(ByVal Element As XElement) As Dictionary(Of Adresse, TypeAdresse) Dim valeur As String Return Element.Descendants("Adresse").ToDictionary(Function(a) New Adresse(a), Function(a)If(a.AttributExits("Type", valeur), CType(System.Enum.Parse(GetType(TypeAdresse),valeur), TypeAdresse), TypeAdresse.Autre)) End Function Public Shared Function Serialise(ByVal Adresses As Dictionary(Of Adresse,TypeAdresse)) As XElement Return New XElement("Adresses", From a In Adresses Select New XElement("Adresse", New XAttribute("Type", a.Value), New XElement("Numero", a.Key.Numero), New XElement("Voie", a.Key.Voie), New XElement("CodePostal", a.Key.CodePostal), New XElement("Ville", a.Key.Ville))) End Function
Linq to Xml, c'est d'abord Linq, si vous l'utilisez déjà dans vos applications, alors cela coulera de source, si ne vous l'utilisez pas encore, qu'attendez-vous?
Je me suis concentré sur le Xml, mais on peut, et c'est toute la force de Linq, mettre des requêtes de filtrage, regroupement ou jointure, dans une requête Xml.
Oui on peut être amené à écrire beaucoup de code, mais pour les balises absentes, par exemple, c'est fait une fois pour toute, il vous suffit de référencer votre classe d'extension à un nouveau projet et c'est tout. Et l'on peut en une ou deux requêtes lire le contenu de plusieurs sources de données (BDD, collections d'objets, liste de fichiers du disques dur, etc...) et en sortir un xml!
Vous pouvez télécharger le projet d'exemple ici