Linq To Xml C# et VB.Net

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.

Introduction

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.

  • L'écriture en ligne, avec expressions lambda ou délégués
//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))
  • L'écriture "spaghetti"
//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.

La classe Personne

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).

Rappel, instancier un objet

Il existe de nombreuses façon d'instancier un objet, en voici quelques unes, dont certaines seront utilisées par la suite:

  • Utilisation du constructeur par défaut et affectation des propriétés une à une
//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".

  • Utilisation d'un constructeur par avec paramètres

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")
  • Utilisation des accolades (merci d'avance à celui qui saura me donner le nom de cette méthode).

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.

  • Utilisation d'une méthode de classe (méthode static en C# et Shared en VB)

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 xml

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é.

Sérialisation

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.

Dessérialisation

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.

Les énumérations

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)

Gestion des valeurs nulles

On a vu plus haut que la sérialisation de valeurs nulles peuvent poser des soucis.
Il faut donc les gérer.

Valeur par défaut

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.

Elément Nullable

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.

Dessérialisation

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
  • soit associer une variable interne à la propriété.
//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")

Sérialisation

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))

Champ de type personnalisé

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"))

Une collection dans ma collection

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

Conclusions

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

A voir également
Ce document intitulé « Linq To Xml C# et VB.Net » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous