Accueil > .Net, C#, Développement, XML, XSD > Jouer avec le XML – partie 3 – (dé)sérialisation de classes et attributs

Jouer avec le XML – partie 3 – (dé)sérialisation de classes et attributs

Troisième et dernier billet au sujet du XML.

Dans ce billet, nous allons voir quelques attributs XML à positionner sur les classes/propriétés et comment (dé)sérialisation une classe via une classe générique.

Voici les billets en rapport :

 

Sérialisation de classes

 

Il y a une classe que je traîne depuis pas mal de temps et qui fonctionne assez bien.
Je l’ai modifié suffisamment pour ne plus retrouver l’originale (que j’avais trouvé sur Internet).

Elle permet de sérialiser pas mal de choses, des objets comme des collections.
Après, il faut quand même que la classe la plus haute (celle qui est imposée par la généricité) soit sérialisable et posséder un constructeur par défaut (sans paramètre).
Je ne m’étends pas plus sur le code puisqu’il est commenté.

using System;
using System.Data;
using System.IO;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace KR.Sample
{
    /// <summary>
    /// Classe StringWriter avec prise en charge de l'encoding.
    /// </summary>
    internal class StringWriterEncode : StringWriter
    {
        #region Propriétés
        private Encoding _Encoding;

        /// <summary>
        /// Encodage du fichier cible.
        /// </summary>
        public override Encoding Encoding { get { return _Encoding; } }
        #endregion

        #region Constructeurs

        /// <summary>
        /// Constructeur par défaut utilisant l'encodage UTF8.
        /// </summary>
        public StringWriterEncode() :
            this(Encoding.UTF8)
        {
        }

        /// <summary>
        /// Constructeur permettant de spécifier l'encodage.
        /// </summary>
        /// <param name="encoding">Encodage souhaité.</param>
        public StringWriterEncode(Encoding encoding)
        {
            _Encoding = encoding ?? Encoding.UTF8;
        }

        #endregion
    }

    /// <summary>
    /// Classe permettant de sérialiser / désérialiser des objets.
    /// </summary>
    public sealed class GenericSerializer<T>
    {
        #region Méthodes statiques publiques

        /// <summary> 
        /// Permet la sérialisation d'un objet.
        /// </summary> 
        /// <param name="objet">Objet à sérialiser.</param>
        /// <param name="encoding">Encodage à utiliser (par défaut : UTF8).</param>
        /// <returns>Une chaine de caractère contenant le XML.</returns> 
        public static String SerialiseObject(T objet, Encoding encoding = null)
        {
            Type type = typeof(T);

            if (!TypeIsSerializable(type))
                throw new InvalidOperationException(String.Format("Le type {0} n'est pas sérialisable.", type.Name));

            XmlSerializer serializer = new XmlSerializer(typeof(T));

            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Encoding = encoding ?? Encoding.UTF8;
            settings.Indent = true;
            settings.OmitXmlDeclaration = false;

            using (TextWriter textWriter = new StringWriterEncode(encoding))
            {
                using (XmlWriter xmlWriter = XmlWriter.Create(textWriter, settings))
                {
                    XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
                    ns.Add(String.Empty, String.Empty);
                    serializer.Serialize(xmlWriter, objet, ns);
                }
                return textWriter.ToString();
            }
        }

        /// <summary> 
        /// Permet de désérialiser une chaine de caractère contenant du XML vers un objet.
        /// </summary> 
        /// <param name="xmlFromSerialisation">XML sérialisé qui représente l'objet.</param> 
        /// <returns>Renvoi un objet type de type T.</returns> 
        public static T DeserialiseObject(String xmlFromSerialisation)
        {
            Type type = typeof(T);

            if (!TypeIsSerializable(type))
                throw new InvalidOperationException(String.Format("Le type {0} n'est pas sérialisable.", type.Name));

            XmlSerializer xs = new XmlSerializer(type);

            if (String.IsNullOrWhiteSpace(xmlFromSerialisation))
                throw new ArgumentException("Le XML fournit est vide.", "xmlFromSerialisation");

            using (StreamReader sr = new StreamReader(xmlFromSerialisation, true))
            {
                return (T)xs.Deserialize(sr);
            }
        }

        /// <summary>
        /// Permet de sérialiser un objet et d'écrire dans un fichier.
        /// </summary>
        /// <param name="objet">Objet à sérialiser.</param>
        /// <param name="filePath">Chemin complet du fichier.</param>
        /// <param name="encoding">Encodage à utiliser (par défaut : UTF8).</param>
        public static void SerialiseAndWrite(T objet, String filePath, Encoding encoding = null)
        {
            String xml = GenericSerializer<T>.SerialiseObject(objet);
            File.WriteAllText(filePath, xml);
        }

        #endregion

        #region Méthodes privées

        /// <summary>
        /// Permet de tester si un type spécifique est sérialisable.
        /// </summary>
        /// <param name="type">Type à tester.</param>
        /// <returns>True si le type est sérialisable.</returns>
        private static bool TypeIsSerializable(Type type)
        {
            // Si, de base, le type n'est pas sérialisable, on sort.
            if (!type.IsSerializable)
                return false;
            // Si c'est un type valeur, on sort.
            if (type.IsValueType)
                return true;

            // Si c'est une classe, on teste un peu plus en profondeur
            // Pour qu'une classe soit sérialisable, il faut que toutes les propriétés de cette classe le soient (récursif).
            if (type.IsClass)
            {
                // Classes implémentant ICollection ou IEnumerable, sérialisable par défaut.
                if (type.GetInterface("ICollection") != null)
                    return true;
                if (type.GetInterface("IEnumerable") != null)
                    return true;

                //Permet de gérer l'exception de sérialisation :
                //"Impossible de sérialiser XXX, car il n'a pas de constructeur sans paramètres."
                if (type.GetConstructor(new Type[0]) == null)
                    return false;

                // On récupère les propriétés pour les parser.
                MemberInfo[] memberInfo = type.GetProperties();
                for (int i = 0; i < memberInfo.Length; i++)
                {
                    PropertyInfo propInfo = (PropertyInfo)memberInfo[i];

                    // Si c'est une classe, on appelle la présente méthode
                    // pour tester si la classe est sérialisable.
                    if (propInfo.PropertyType.IsClass)
                    {
                        bool inner = TypeIsSerializable(propInfo.PropertyType);
                        // Si un membre du Type empêche la sérialisation
                        // le Type n'est pas sérialisable.
                        if (!inner) return false;
                    }
                    else
                    {
                        // Si, de base, le type n'est pas sérialisable, on sort.
                        if (!propInfo.PropertyType.IsSerializable)
                            return false;

                        //Objets DataSet, sérialisable.
                        if (propInfo.PropertyType == typeof(DataSet))
                            return true;

                        // Si le membre est tagué avec l'attribut XmlIgnore.
                        // Alors, il ne sera pas sérialisé.
                        // Donc, on l'ignore dans le cadre de ce test.
                        object[] xmlIgnoreAttribute = propInfo.PropertyType.GetCustomAttributes(typeof(XmlIgnoreAttribute), true);
                        if (xmlIgnoreAttribute == null || xmlIgnoreAttribute.Length == 0)
                        {
                            //Propriétés en lecture/écriture publiques.
                            if (!propInfo.PropertyType.IsPublic)
                                return false;
                            if (!propInfo.CanRead || !propInfo.CanWrite)
                                return false;
                            if (propInfo.GetSetMethod(false) == null || propInfo.GetGetMethod(false) == null)
                                return false;
                        }
                    }
                }
            }
            return true;
        }

        #endregion
    }
}

 

Attributs XML

 

Le premier attribut à connaître est le fameux SerializableAttribute.
Cet attribut est à positionner sur les classes.

Pour ce billet, on va travailler sur ces classes :

using System;
using System.Collections.Generic;

namespace KR.Sample
{
    [Serializable]
    public class User
    {
        public String Identifiant { get; set; }
        public String LastName { get; set; }
        public String FirstName { get; set; }
        public String Login { get; set; }
        public String MailAdress { get; set; }
        public String Comment { get; set; }
    }
    [Serializable]
    public class Header
    {
        public Int32 NumberOfUsers { get; set; }
        public DateTime ExportDate { get; set; }
    }
    [Serializable]
    public class UserList
    {
        public Header HeaderPart { get; set; }
        public List<User> UsersList { get; set; }

        public void FillUsers()
        {
            HeaderPart = new Header() { ExportDate = DateTime.Now, NumberOfUsers = 2 };
            UsersList = new List<User>()
            {
                new User(){ Identifiant = "0000000001", FirstName = "FABIEN", 
                            LastName = "GUYOT", Login = "fguyot", 
                            MailAdress = "fguyot@sample.com" },
                new User(){ Identifiant = "0000000002", FirstName = "JOHN", 
                            LastName = "DOE", Login = "jdoe", 
                            MailAdress = "jdoe@sample.com",
                            Comment = "Le compte est actuellement suspendu & doit être désactivé."}
            };
        }
    }
}

La méthode FillUsers() est juste présente pour alimenter les données.

Si l’on utilise la méthode de sérialisation en début de billet, on obtient :

GenericSerializer<UserList>.SerialiseAndWrite(users, @"C:\Projects\KR.Samples\XSD\SampleXML2.xml", null);
<?xml version="1.0" encoding="utf-8"?>
<UserList>
  <HeaderPart>
    <NumberOfUsers>2</NumberOfUsers>
    <ExportDate>2012-11-29T14:39:23.5142765+01:00</ExportDate>
  </HeaderPart>
  <UsersList>
    <User>
      <Identifiant>0000000001</Identifiant>
      <LastName>GUYOT</LastName>
      <FirstName>FABIEN</FirstName>
      <Login>fguyot</Login>
      <MailAdress>fguyot@sample.com</MailAdress>
    </User>
    <User>
      <Identifiant>0000000002</Identifiant>
      <LastName>DOE</LastName>
      <FirstName>JOHN</FirstName>
      <Login>jdoe</Login>
      <MailAdress>jdoe@sample.com</MailAdress>
      <Comment>Le compte est actuellement suspendu &amp; doit être désactivé.</Comment>
    </User>
  </UsersList>
</UserList>

Comme on peut le voir, ce n’est pas du tout le même XML qu’attendu (voir les deux autres billets).

Donc, on va déjà modifier les noms des balises.
Pour ce faire, il faut utiliser l’attribut XmlElement du namespace System.Xml.Serialization.
Cet attribut ne fonctionne cependant pas sur les classes, mais uniquement sur les propriétés.
Donc, sur la classe UserList, on va utiliser l’attribut XmlRoot.

    [Serializable]
    public class User
    {
        [XmlElement("IDENTIFIANT")]
        public String Identifiant { get; set; }
        [XmlElement("NOM")]
        public String LastName { get; set; }
        [XmlElement("PRENOM")]
        public String FirstName { get; set; }
        [XmlElement("LOGIN")]
        public String Login { get; set; }
        [XmlElement("MAIL")]
        public String MailAdress { get; set; }
        [XmlElement("COMMENT")]
        public String Comment { get; set; }
    }
    [Serializable]
    public class Header
    {
        [XmlElement("NBUSERS")]
        public Int32 NumberOfUsers { get; set; }
        [XmlElement("DATEEXPORT")]
        public DateTime ExportDate { get; set; }
    }
    [Serializable]
    [XmlRoot("EXPORT")]
    public class UserList
    {
        [XmlElement("HEADER")]
        public Header HeaderPart { get; set; }
        public List<User> UsersList { get; set; }
    }

Voici le XML :

<?xml version="1.0" encoding="utf-8"?>
<EXPORT>
  <HEADER>
    <NBUSERS>2</NBUSERS>
    <DATEEXPORT>2012-11-29T14:53:54.8816685+01:00</DATEEXPORT>
  </HEADER>
  <UsersList>
    <User>
      <IDENTIFIANT>0000000001</IDENTIFIANT>
      <NOM>GUYOT</NOM>
      <PRENOM>FABIEN</PRENOM>
      <LOGIN>fguyot</LOGIN>
      <MAIL>fguyot@sample.com</MAIL>
    </User>
    <User>
      <IDENTIFIANT>0000000002</IDENTIFIANT>
      <NOM>DOE</NOM>
      <PRENOM>JOHN</PRENOM>
      <LOGIN>jdoe</LOGIN>
      <MAIL>jdoe@sample.com</MAIL>
      <COMMENT>Le compte est actuellement suspendu &amp; doit être désactivé.</COMMENT>
    </User>
  </UsersList>
</EXPORT>

A très peu de frais, on s’est bien rapproché de ce qui est attendu.

Maintenant, penchons-nous sur la liste.
Pour obtenir le rendu attendu, il suffit à nouveau d’utiliser l’attribut XmlElement :

        [XmlElement("USER")]
        public List<User> UsersList { get; set; }

Mais il existe quand même deux autres attributs intéressants : XmlArray et XmlArrayItem.
Le premier va modifier la balise désignant la liste tandis que le second le nom de chaque élément.
Comme un exemple est plus parlant :

        [XmlArray("USERS")]
        [XmlArrayItem("USER")]
        public List<User> UsersList { get; set; }
<?xml version="1.0" encoding="utf-8"?>
<EXPORT>
  <HEADER>
    <NBUSERS>2</NBUSERS>
    <DATEEXPORT>2012-11-29T14:57:17.9459865+01:00</DATEEXPORT>
  </HEADER>
  <USERS>
    <USER>
      <IDENTIFIANT>0000000001</IDENTIFIANT>
      <NOM>GUYOT</NOM>
      <PRENOM>FABIEN</PRENOM>
      <LOGIN>fguyot</LOGIN>
      <MAIL>fguyot@sample.com</MAIL>
    </USER>
    <USER>
      <IDENTIFIANT>0000000002</IDENTIFIANT>
      <NOM>DOE</NOM>
      <PRENOM>JOHN</PRENOM>
      <LOGIN>jdoe</LOGIN>
      <MAIL>jdoe@sample.com</MAIL>
      <COMMENT>Le compte est actuellement suspendu &amp; doit être désactivé.</COMMENT>
    </USER>
  </USERS>
</EXPORT>

Reste donc le problème du format de date.
Et là, c’est nettement plus compliqué car il n’existe pas d’attribut qui gère le format automatiquement.

Donc, on peut sortir l’usine à gaz : créer un attribut puis se créer un sérialiseur, le tout à la mano.

Sinon, on peut ajouter une propriété de type string et qui le fait…
Ce n’est pas très élégant, mais ça à le mérite de bien fonctionner !

        [XmlIgnore]
        public DateTime ExportDate { get; set; }
        [XmlElement("DATEEXPORT")]
        public String StringDate
        {
            get { return ExportDate.ToString("ddmmyyyy"); }
            set { DateTime.ParseExact(value, "ddmmyyyy", CultureInfo.InvariantCulture); }
        }

Comme son nom l’indique, l’attribut XmlIgnore permet…d’ignorer le champ lors de la sérialisation.
Et le résultat :

<?xml version="1.0" encoding="utf-8"?>
<EXPORT>
  <HEADER>
    <NBUSERS>2</NBUSERS>
    <DATEEXPORT>29102012</DATEEXPORT>
  </HEADER>
  <USER>
    <IDENTIFIANT>0000000001</IDENTIFIANT>
    <NOM>GUYOT</NOM>
    <PRENOM>FABIEN</PRENOM>
    <LOGIN>fguyot</LOGIN>
    <MAIL>fguyot@sample.com</MAIL>
  </USER>
  <USER>
    <IDENTIFIANT>0000000002</IDENTIFIANT>
    <NOM>DOE</NOM>
    <PRENOM>JOHN</PRENOM>
    <LOGIN>jdoe</LOGIN>
    <MAIL>jdoe@sample.com</MAIL>
    <COMMENT>Le compte est actuellement suspendu &amp; doit être désactivé.</COMMENT>
  </USER>
</EXPORT>

Et voilà, mission accomplie : on a un XML assez similaire à celui souhaité.

 

Héritage

 

Et est-ce que la méthode de sérialisation donnée plus haut fonctionne avec l’héritage ?
Oui, bien sûr !

Exemple :

    [Serializable]
    [XmlRoot("GOD")]
    public class Admin : User
    {
        [XmlElement("ALLMIGHTY")]
        public Boolean SuperAdmin { get; set; }
    }

Ce qui donne :

<?xml version="1.0" encoding="utf-8"?>
<GOD>
  <IDENTIFIANT>007</IDENTIFIANT>
  <NOM>POND</NOM>
  <PRENOM>JAMES</PRENOM>
  <LOGIN>Vectordean</LOGIN>
  <MAIL>jpond@sample.com</MAIL>
  <ALLMIGHTY>true</ALLMIGHTY>
</GOD>

Toute référence cachée et bien évidemment le fruit du plus parfait hasard. Et pour ceux qui ne connaissent pas…eh bah je me prends un bon coup de vieux ! ^^

Catégories :.Net, C#, Développement, XML, XSD
  1. Aucun commentaire pour l’instant.
  1. 30/11/2012 à 20:50

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :