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

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 .


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
}

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/


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

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

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.


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

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+


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


Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.