Sérialiser la classe contenant le membre du dictionnaire

144

En développant mon problème précédent , j'ai décidé de (dé) sérialiser ma classe de fichier de configuration qui fonctionnait très bien.

Je veux maintenant stocker un tableau associatif de lettres de lecteur à carte (clé est la lettre du lecteur, la valeur est le chemin réseau) et ont essayé d' utiliser Dictionary, HybridDictionaryet Hashtablepour cela , mais je reçois toujours l'erreur suivante lors de l' appel ConfigFile.Load()ou ConfigFile.Save():

Une erreur s'est produite dans le type "App.ConfigFile". [snip] System.NotSupportedException: impossible de sérialiser le membre App.Configfile.mappedDrives [snip]

D'après ce que j'ai lu, les dictionnaires et les HashTables peuvent être sérialisés, alors qu'est-ce que je fais de mal?

[XmlRoot(ElementName="Config")]
public class ConfigFile
{
    public String guiPath { get; set; }
    public string configPath { get; set; }
    public Dictionary<string, string> mappedDrives = new Dictionary<string, string>();

    public Boolean Save(String filename)
    {
        using(var filestream = File.Open(filename, FileMode.OpenOrCreate,FileAccess.ReadWrite))
        {
            try
            {
                var serializer = new XmlSerializer(typeof(ConfigFile));
                serializer.Serialize(filestream, this);
                return true;
            } catch(Exception e) {
                MessageBox.Show(e.Message);
                return false;
            }
        }
    }

    public void addDrive(string drvLetter, string path)
    {
        this.mappedDrives.Add(drvLetter, path);
    }

    public static ConfigFile Load(string filename)
    {
        using (var filestream = File.Open(filename, FileMode.Open, FileAccess.Read))
        {
            try
            {
                var serializer = new XmlSerializer(typeof(ConfigFile));
                return (ConfigFile)serializer.Deserialize(filestream);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message + ex.ToString());
                return new ConfigFile();
            }
        }
    }
}
dragonmantank
la source

Réponses:

77

Vous ne pouvez pas sérialiser une classe qui implémente IDictionary. Consultez ce lien .

Q: Pourquoi ne puis-je pas sérialiser les tables de hachage?

R: Le XmlSerializer ne peut pas traiter les classes implémentant l'interface IDictionary. Cela était en partie dû à des contraintes de calendrier et en partie au fait qu'une table de hachage n'a pas d'équivalent dans le système de type XSD. La seule solution consiste à implémenter une table de hachage personnalisée qui n'implémente pas l'interface IDictionary.

Je pense donc que vous devez créer votre propre version du dictionnaire pour cela. Vérifiez cette autre question .

bruno conde
la source
4
Je me demande simplement que la DataContractSerializerclasse puisse le faire. Juste la sortie est un peu moche.
rekire le
186

Il existe une solution sur le Weblog de Paul Welter - Dictionnaire générique XML sérialisable

Pour une raison quelconque, le dictionnaire générique dans .net 2.0 n'est pas sérialisable XML. L'extrait de code suivant est un dictionnaire générique sérialisable xml. Le dictionnaire peut être sérialisé en implémentant l'interface IXmlSerializable.

using System;
using System.Collections.Generic;
using System.Text;
using System.Xml.Serialization;

[XmlRoot("dictionary")]
public class SerializableDictionary<TKey, TValue>
    : Dictionary<TKey, TValue>, IXmlSerializable
{
    public SerializableDictionary() { }
    public SerializableDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary) { }
    public SerializableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer) { }
    public SerializableDictionary(IEqualityComparer<TKey> comparer) : base(comparer) { }
    public SerializableDictionary(int capacity) : base(capacity) { }
    public SerializableDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer) { }

    #region IXmlSerializable Members
    public System.Xml.Schema.XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(System.Xml.XmlReader reader)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        bool wasEmpty = reader.IsEmptyElement;
        reader.Read();

        if (wasEmpty)
            return;

        while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
        {
            reader.ReadStartElement("item");

            reader.ReadStartElement("key");
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadStartElement("value");
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();

            this.Add(key, value);

            reader.ReadEndElement();
            reader.MoveToContent();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(System.Xml.XmlWriter writer)
    {
        XmlSerializer keySerializer = new XmlSerializer(typeof(TKey));
        XmlSerializer valueSerializer = new XmlSerializer(typeof(TValue));

        foreach (TKey key in this.Keys)
        {
            writer.WriteStartElement("item");

            writer.WriteStartElement("key");
            keySerializer.Serialize(writer, key);
            writer.WriteEndElement();

            writer.WriteStartElement("value");
            TValue value = this[key];
            valueSerializer.Serialize(writer, value);
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
    }
    #endregion
}
osman pirci
la source
16
+1. Hé, pourquoi Stack Overflow n'a-t-il pas de bouton de copie de code? Hmmm? car ce code vaut la peine d'être copié!
toddmo
1
+1 Réponse fantastique. Fonctionne également pour SortedList, il suffit de changer le "SerializableDictionary" en "SerializableSortedList" et le "Dictionary <TKey, TValue>" en "SortedList <TKey, TValue>".
kdmurray
1
+1 et une suggestion. Lorsqu'un objet SerializableDictionary contient plusieurs éléments, une exception est levée ... ReadXml () et WriteXml () doivent être modifiés pour déplacer ReadStartElement ("item"); et WriteStartElement ("élément"); et ses ReadEndElement () et WriteEndElement () associés hors de la boucle while.
MNS
1
Cela signifie-t-il alors que dans les frameworks ultérieurs, IDictionary est sérialisable?
Thomas
1
Cette implémentation fonctionnera si le dictionnaire stocke, par exemple, des stringvaleurs, mais lancera une InvalidOperationExceptiondésérialisation en mentionnant un élément wrapper inattendu si vous essayez d'y stocker des objets personnalisés ou des tableaux de chaînes. (Voir ma question pour un exemple des problèmes auxquels j'ai été confronté.)
Christopher Kyle Horton
57

Au lieu d'utiliser, XmlSerializervous pouvez utiliser un fichier System.Runtime.Serialization.DataContractSerializer. Cela peut sérialiser des dictionnaires et des interfaces sans effort.

Voici un lien vers un exemple complet, http://theburningmonk.com/2010/05/net-tips-xml-serialize-or-deserialize-dictionary-in-csharp/

Despertar
la source
2
Meilleure réponse, haut la main.
DWRoelands
D'accord, c'est la meilleure réponse. Propre, simple et SEC (ne vous répétez pas).
Nolonar
Le problème avec DataContractSerializer est qu'il sérialise et désérialise par ordre alphabétique, donc si vous essayez de désérialiser quelque chose dans le mauvais ordre, il échouera silencieusement avec des propriétés nulles dans votre objet
superjugy
14

Créez un substitut de sérialisation.

Exemple, vous avez une classe avec une propriété publique de type Dictionary.

Pour prendre en charge la sérialisation Xml de ce type, créez une classe clé-valeur générique:

public class SerializeableKeyValue<T1,T2>
{
    public T1 Key { get; set; }
    public T2 Value { get; set; }
}

Ajoutez un attribut XmlIgnore à votre propriété d'origine:

    [XmlIgnore]
    public Dictionary<int, string> SearchCategories { get; set; }

Exposez une propriété publique de type tableau, qui contient un tableau d'instances SerializableKeyValue, qui sont utilisées pour sérialiser et désérialiser dans la propriété SearchCategories:

    public SerializeableKeyValue<int, string>[] SearchCategoriesSerializable
    {
        get
        {
            var list = new List<SerializeableKeyValue<int, string>>();
            if (SearchCategories != null)
            {
                list.AddRange(SearchCategories.Keys.Select(key => new SerializeableKeyValue<int, string>() {Key = key, Value = SearchCategories[key]}));
            }
            return list.ToArray();
        }
        set
        {
            SearchCategories = new Dictionary<int, string>();
            foreach (var item in value)
            {
                SearchCategories.Add( item.Key, item.Value );
            }
        }
    }
user2921681
la source
J'aime cela car il dissocie la sérialisation du membre du dictionnaire. Si j'avais une classe très utilisée à laquelle je souhaitais ajouter des capacités de sérialisation, l'encapsulation du dictionnaire pourrait provoquer une rupture avec les types hérités.
VoteCoffee
Un mot d'avertissement pour quiconque implémente ceci: si vous essayez de faire de votre propriété de substitution une List (ou toute autre collection ), le sérialiseur XML n'appellera pas le setter (à la place, il appelle le getter et essaie d'ajouter à la liste renvoyée, ce qui n'est évidemment pas ce que vous vouliez). Tenez-vous-en aux tableaux pour ce modèle.
Fraxtil
9

Vous devriez explorer Json.Net, assez facile à utiliser et qui permet aux objets Json d'être désérialisés directement dans Dictionary.

james_newtonking

exemple:

string json = @"{""key1"":""value1"",""key2"":""value2""}";
Dictionary<string, string> values = JsonConvert.DeserializeObject<Dictionary<string, string>>(json); 
Console.WriteLine(values.Count);
// 2
Console.WriteLine(values["key1"]);
// value1
Jean-Philippe Gravel
la source
6

Les dictionnaires et les tables de hachage ne sont pas sérialisables avec XmlSerializer. Par conséquent, vous ne pouvez pas les utiliser directement. Une solution de contournement serait d'utiliser leXmlIgnore attribut pour masquer ces propriétés du sérialiseur et les exposer via une liste de paires clé-valeur sérialisables.

PS: construire un XmlSerializerest très coûteux, donc mettez-le toujours en cache s'il y a une chance de pouvoir le réutiliser.

David Schmitt
la source
4

Je voulais une classe SerializableDictionary qui utilisait des attributs xml pour la clé / valeur, j'ai donc adapté la classe de Paul Welter.

Cela produit du XML comme:

<Dictionary>
  <Item Key="Grass" Value="Green" />
  <Item Key="Snow" Value="White" />
  <Item Key="Sky" Value="Blue" />
</Dictionary>"

Code:

using System.Collections.Generic;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Serialization;

namespace DataTypes {
    [XmlRoot("Dictionary")]
    public class SerializableDictionary<TKey, TValue>
        : Dictionary<TKey, TValue>, IXmlSerializable {
        #region IXmlSerializable Members
        public System.Xml.Schema.XmlSchema GetSchema() {
            return null;
        }

        public void ReadXml(XmlReader reader) {
            XDocument doc = null;
            using (XmlReader subtreeReader = reader.ReadSubtree()) {
                doc = XDocument.Load(subtreeReader);
            }
            XmlSerializer serializer = new XmlSerializer(typeof(SerializableKeyValuePair<TKey, TValue>));
            foreach (XElement item in doc.Descendants(XName.Get("Item"))) {
                using(XmlReader itemReader =  item.CreateReader()) {
                    var kvp = serializer.Deserialize(itemReader) as SerializableKeyValuePair<TKey, TValue>;
                    this.Add(kvp.Key, kvp.Value);
                }
            }
            reader.ReadEndElement();
        }

        public void WriteXml(System.Xml.XmlWriter writer) {
            XmlSerializer serializer = new XmlSerializer(typeof(SerializableKeyValuePair<TKey, TValue>));
            XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
            ns.Add("", "");
            foreach (TKey key in this.Keys) {
                TValue value = this[key];
                var kvp = new SerializableKeyValuePair<TKey, TValue>(key, value);
                serializer.Serialize(writer, kvp, ns);
            }
        }
        #endregion

        [XmlRoot("Item")]
        public class SerializableKeyValuePair<TKey, TValue> {
            [XmlAttribute("Key")]
            public TKey Key;

            [XmlAttribute("Value")]
            public TValue Value;

            /// <summary>
            /// Default constructor
            /// </summary>
            public SerializableKeyValuePair() { }
        public SerializableKeyValuePair (TKey key, TValue value) {
            Key = key;
            Value = value;
        }
    }
}
}

Tests unitaires:

using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace DataTypes {
    [TestClass]
    public class SerializableDictionaryTests {
        [TestMethod]
        public void TestStringStringDict() {
            var dict = new SerializableDictionary<string, string>();
            dict.Add("Grass", "Green");
            dict.Add("Snow", "White");
            dict.Add("Sky", "Blue");
            dict.Add("Tomato", "Red");
            dict.Add("Coal", "Black");
            dict.Add("Mud", "Brown");

            var serializer = new System.Xml.Serialization.XmlSerializer(dict.GetType());
            using (var stream = new MemoryStream()) {
                // Load memory stream with this objects xml representation
                XmlWriter xmlWriter = null;
                try {
                    xmlWriter = XmlWriter.Create(stream);
                    serializer.Serialize(xmlWriter, dict);
                } finally {
                    xmlWriter.Close();
                }

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);

                XDocument doc = XDocument.Load(stream);
                Assert.AreEqual("Dictionary", doc.Root.Name);
                Assert.AreEqual(dict.Count, doc.Root.Descendants().Count());

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);
                var outDict = serializer.Deserialize(stream) as SerializableDictionary<string, string>;
                Assert.AreEqual(dict["Grass"], outDict["Grass"]);
                Assert.AreEqual(dict["Snow"], outDict["Snow"]);
                Assert.AreEqual(dict["Sky"], outDict["Sky"]);
            }
        }

        [TestMethod]
        public void TestIntIntDict() {
            var dict = new SerializableDictionary<int, int>();
            dict.Add(4, 7);
            dict.Add(5, 9);
            dict.Add(7, 8);

            var serializer = new System.Xml.Serialization.XmlSerializer(dict.GetType());
            using (var stream = new MemoryStream()) {
                // Load memory stream with this objects xml representation
                XmlWriter xmlWriter = null;
                try {
                    xmlWriter = XmlWriter.Create(stream);
                    serializer.Serialize(xmlWriter, dict);
                } finally {
                    xmlWriter.Close();
                }

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);

                XDocument doc = XDocument.Load(stream);
                Assert.AreEqual("Dictionary", doc.Root.Name);
                Assert.AreEqual(3, doc.Root.Descendants().Count());

                // Rewind
                stream.Seek(0, SeekOrigin.Begin);
                var outDict = serializer.Deserialize(stream) as SerializableDictionary<int, int>;
                Assert.AreEqual(dict[4], outDict[4]);
                Assert.AreEqual(dict[5], outDict[5]);
                Assert.AreEqual(dict[7], outDict[7]);
            }
        }
    }
}
Keyo
la source
1
Ça a l'air bien mais ça échoue avec un dictionnaire vide. Vous avez besoin du test reader.IsEmptyElement dans la méthode ReadXML.
AnthonyVO
2

la classe Dictionary implémente ISerializable. La définition du dictionnaire de classe donnée ci-dessous.

[DebuggerTypeProxy(typeof(Mscorlib_DictionaryDebugView<,>))]
[DebuggerDisplay("Count = {Count}")]
[Serializable]
[System.Runtime.InteropServices.ComVisible(false)]
public class Dictionary<TKey,TValue>: IDictionary<TKey,TValue>, IDictionary, IReadOnlyDictionary<TKey, TValue>, ISerializable, IDeserializationCallback  

Je ne pense pas que ce soit le problème. reportez-vous au lien ci-dessous, qui indique que si vous avez un autre type de données qui n'est pas sérialisable, le dictionnaire ne sera pas sérialisé. http://forums.asp.net/t/1734187.aspx?Is+Dictionary+serializable+

Saikrishna
la source
C'est vrai dans les dernières versions, mais dans .NET 2, Dictionary n'est pas sérialisable, même aujourd'hui. Je l'ai confirmé aujourd'hui avec un projet ciblant .NET 3.5, c'est ainsi que j'ai trouvé ce fil.
Bruce
2

Vous pouvez utiliser ExtendedXmlSerializer . Si vous avez un cours:

public class ConfigFile
{
    public String guiPath { get; set; }
    public string configPath { get; set; }
    public Dictionary<string, string> mappedDrives {get;set;} 

    public ConfigFile()
    {
        mappedDrives = new Dictionary<string, string>();
    }
}

et créez une instance de cette classe:

ConfigFile config = new ConfigFile();
config.guiPath = "guiPath";
config.configPath = "configPath";
config.mappedDrives.Add("Mouse", "Logitech MX Master");
config.mappedDrives.Add("keyboard", "Microsoft Natural Ergonomic Keyboard 4000");

Vous pouvez sérialiser cet objet à l'aide d'ExtendedXmlSerializer:

ExtendedXmlSerializer serializer = new ExtendedXmlSerializer();
var xml = serializer.Serialize(config);

Le XML de sortie ressemblera à:

<?xml version="1.0" encoding="utf-8"?>
<ConfigFile type="Program+ConfigFile">
    <guiPath>guiPath</guiPath>
    <configPath>configPath</configPath>
    <mappedDrives>
        <Item>
            <Key>Mouse</Key>
            <Value>Logitech MX Master</Value>
        </Item>
        <Item>
            <Key>keyboard</Key>
            <Value>Microsoft Natural Ergonomic Keyboard 4000</Value>
        </Item>
    </mappedDrives>
</ConfigFile>

Vous pouvez installer ExtendedXmlSerializer à partir de nuget ou exécuter la commande suivante:

Install-Package ExtendedXmlSerializer

Voici un exemple en ligne

Wojtpl2
la source