Serialize Class containing Dictionary member

Viewed 172910

Expanding upon my earlier problem, I've decided to (de)serialize my config file class which worked great.

I now want to store an associative array of drive letters to map (key is the drive letter, value is the network path) and have tried using Dictionary, HybridDictionary, and Hashtable for this but I always get the following error when calling ConfigFile.Load() or ConfigFile.Save():

There was an error reflecting type 'App.ConfigFile'. [snip] System.NotSupportedException: Cannot serialize member App.Configfile.mappedDrives [snip]

From what I've read Dictionaries and HashTables can be serialized, so what am I doing wrong?

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

You can't serialize a class that implements IDictionary. Check out this link.

Q: Why can't I serialize hashtables?

A: The XmlSerializer cannot process classes implementing the IDictionary interface. This was partly due to schedule constraints and partly due to the fact that a hashtable does not have a counterpart in the XSD type system. The only solution is to implement a custom hashtable that does not implement the IDictionary interface.

So I think you need to create your own version of the Dictionary for this. Check this other question.

Dictionaries and Hashtables are not serializable with XmlSerializer. Therefore you cannot use them directly. A workaround would be to use the XmlIgnore attribute to hide those properties from the serializer and expose them via a list of serializable key-value pairs.

PS: constructing an XmlSerializer is very expensive, so always cache it if there is a chance of being able to re-use it.

You can use ExtendedXmlSerializer. If you have a class:

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

and create instance of this class:

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

You can serialize this object using ExtendedXmlSerializer:

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

Output xml will look like:

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

You can install ExtendedXmlSerializer from nuget or run the following command:

Install-Package ExtendedXmlSerializer

Here is online example

you can use DataContractSerialize of System.Runtime.Serialization. This will able to serialize IDictionary and Dictionary members.

https://docs.microsoft.com/en-us/dotnet/framework/wcf/samples/datacontractserializer-sample

Find the code snippet below.

public  ConfigFile ExtractConfigFileFromXml(string xmlPath)
{
    var serializer = new DataContractSerializer(typeof(ConfigFile));
    XmlReaderSettings settings = new XmlReaderSettings();
    settings.DtdProcessing = DtdProcessing.Parse;
    XmlReader reader = XmlReader.Create(xmlPath, settings);
    var confile = (ConfigFile)serializer.ReadObject(reader);
    return confile;
 }

Building on the reference to Paul Welter's blog, here is an updated codeset compatible with the latest C# to handle nullable types (because, you know, I'm anal about writing code with no warnings!):

/*-----------------------------------------------------------------------------------------------------
 * Class borrowed from Paul Welter's blog https://weblogs.asp.net/pwelter34/444961
 * Thank you for your invaluable contribution!
 */

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

namespace {Your-Namespace}
{

    [XmlRoot("dictionary")]
    public class SerializableDictionary<TKey, TValue>
        : Dictionary<TKey, TValue>, IXmlSerializable
        where TKey : notnull
    {
        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(typeof(TKey));
            XmlSerializer valueSerializer = new(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(typeof(TKey));
            XmlSerializer valueSerializer = new(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
    }
}

Related