Serializar clase que contiene un miembro del diccionario

144

Expandiendo sobre mi problema anterior , he decidido (des) serializar mi clase de archivo de configuración que funcionó muy bien.

Ahora quiero almacenar una matriz asociativa de las letras de unidad que desea asignar (clave es la letra de unidad, el valor es la ruta de acceso a la red) y han tratado de utilizar Dictionary, HybridDictionaryy Hashtablepara esto, pero siempre me sale el siguiente error al llamar ConfigFile.Load()o ConfigFile.Save():

Se produjo un error al reflejar el tipo 'App.ConfigFile'. [snip] System.NotSupportedException: no se puede serializar el miembro App.Configfile.mappedDrives [snip]

Por lo que he leído, los diccionarios y HashTables se pueden serializar, entonces, ¿qué estoy haciendo 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
fuente

Respuestas:

77

No puede serializar una clase que implementa IDictionary. Mira este enlace .

P: ¿Por qué no puedo serializar tablas hash?

R: XmlSerializer no puede procesar clases que implementan la interfaz IDictionary. Esto se debió en parte a las restricciones de programación y en parte al hecho de que una tabla hash no tiene una contraparte en el sistema de tipo XSD. La única solución es implementar una tabla hash personalizada que no implemente la interfaz IDictionary.

Así que creo que necesitas crear tu propia versión del Diccionario para esto. Mira esta otra pregunta .

bruno conde
fuente
44
Solo me pregunto si la DataContractSerializerclase puede hacer eso. Solo la salida es un poco fea.
rekire
186

Hay una solución en el blog de Paul Welter: Diccionario genérico serializable XML

Por alguna razón, el Diccionario genérico en .net 2.0 no es serializable en XML. El siguiente fragmento de código es un diccionario genérico serializable xml. El diccionario es serializable implementando la interfaz 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
fuente
16
+1. Oye, ¿por qué Stack Overflow no tiene un botón de código de copia? Hmmm? ¡Porque vale la pena copiar este código!
toddmo
1
+1 Fantástica respuesta. También funciona para SortedList, solo cambió "SerializableDictionary" a "SerializableSortedList" y el "Diccionario <TKey, TValue>" a "SortedList <TKey, TValue>".
kdmurray
1
+1 y una sugerencia. Cuando un objeto SerializableDictionary contiene más de un elemento, se produce una excepción ... ReadXml () y WriteXml () deben modificarse para mover ReadStartElement ("item"); y WriteStartElement ("elemento"); y sus ReadEndElement () y WriteEndElement () asociados del ciclo while.
MNS
1
¿Eso significa que en marcos posteriores el IDictionary es serializable?
Thomas
1
Esta implementación funcionará si el diccionario está almacenando, digamos, stringvalores, pero arrojará una InvalidOperationExceptiondeserialización mencionando un elemento contenedor inesperado si intenta almacenar objetos personalizados o matrices de cadenas en él. (Vea mi pregunta para ver un ejemplo de los problemas que enfrenté con esto.)
Christopher Kyle Horton
57

En lugar de usar XmlSerializerpuedes usar unSystem.Runtime.Serialization.DataContractSerializer . Esto puede serializar diccionarios e interfaces sin problemas.

Aquí hay un enlace a un ejemplo completo, http://theburningmonk.com/2010/05/net-tips-xml-serialize-or-deserialize-dictionary-in-csharp/

Despertar
fuente
2
La mejor respuesta, sin duda.
DWRoelands
De acuerdo, esta es la mejor respuesta. Limpio, simple y SECO (no se repita).
Nolonar el
El problema con DataContractSerializer es que serializa y deserializa en orden alfabético, por lo que si intenta deserializar algo en el orden incorrecto, fallará silenciosamente con propiedades nulas en su objeto
superjugy
14

Crea un sustituto de serialización.

Ejemplo, tiene una clase con propiedad pública de tipo Diccionario.

Para admitir la serialización Xml de este tipo, cree una clase genérica de clave-valor:

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

Agregue un atributo XmlIgnore a su propiedad original:

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

Exponga una propiedad pública de tipo de matriz, que contenga una matriz de instancias SerializableKeyValue, que se utilizan para serializar y deserializar en la propiedad 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 );
            }
        }
    }
usuario2921681
fuente
Me gusta esto porque desacopla la serialización del miembro del diccionario. Si tuviera una clase muy utilizada a la que quisiera agregar capacidades de serialización, entonces ajustar el diccionario podría causar una ruptura con los tipos heredados.
VoteCoffee
Una advertencia para cualquiera que implemente esto: si intenta convertir su propiedad sustituta en una Lista (o cualquier otra colección ), el serializador XML no llamará al configurador (en su lugar, llama al captador e intenta agregar a la lista devuelta, que obviamente no es lo que querías). Se adhieren a las matrices para este patrón.
Fraxtil
9

Debe explorar Json.Net, bastante fácil de usar y permite que los objetos Json se deserialicen en el Diccionario directamente.

james_newtonking

ejemplo:

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
fuente
6

Los diccionarios y las tablas hash no son serializables con XmlSerializer. Por lo tanto, no puede usarlos directamente. Una solución alternativa sería utilizar elXmlIgnore atributo para ocultar esas propiedades del serializador y exponerlas a través de una lista de pares clave-valor serializables.

PD: la construcción de un XmlSerializeres muy costosa, así que siempre almacénelo en caché si existe la posibilidad de poder reutilizarlo.

David Schmitt
fuente
4

Quería una clase SerializableDictionary que usara atributos xml para clave / valor, así que adapté la clase de Paul Welter.

Esto produce xml como:

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

Código:

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;
        }
    }
}
}

Pruebas unitarias:

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
fuente
1
Se ve bien pero falla con un diccionario vacío. Necesita la prueba reader.IsEmptyElement en el método ReadXML.
AnthonyVO
2

La clase Diccionario implementa ISerializable. La definición de Diccionario de clase se da a continuación.

[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  

No creo que ese sea el problema. consulte el siguiente enlace, que dice que si tiene cualquier otro tipo de datos que no sea serializable, el Diccionario no se serializará. http://forums.asp.net/t/1734187.aspx?Is+Dictionary+serializable+

Saikrishna
fuente
Eso es cierto en las últimas versiones, pero en .NET 2, Dictionary no es serializable, incluso hoy. Lo confirmé hoy con un proyecto dirigido a .NET 3.5, que es cómo encontré este hilo.
Bruce
2

Puede usar ExtendedXmlSerializer . Si tienes una clase:

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>();
    }
}

y crea una instancia de esta clase:

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

Puede serializar este objeto usando ExtendedXmlSerializer:

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

La salida xml se verá así:

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

Puede instalar ExtendedXmlSerializer desde nuget o ejecutar el siguiente comando:

Install-Package ExtendedXmlSerializer

Aquí hay un ejemplo en línea

Wojtpl2
fuente