Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

XmlSerializer doesn't work with Dictionaries. Oh, and it has problems with KeyValuePairs too.

5.00/5 (1 vote)
13 Jan 2012CPOL1 min read 46.8K  
XmlSerializer complains if you try to serialize anything which implements IDictionary. This provides a way of serialising them which (if not capable of restoring the exact dictionary content) restores the actual field contents in a new dictionary.
All I wanted to do was simple: add a setting to the application config which held a list of the most recently used file names, complete with a user created template to process the file - so they didn't have to repeat work.

Since it is going in the config file (which is XML), it made sense to use the XmlSerializer. Easy:
C#
Dictionary<string, string> recentFiles;
...
xml.Serialize(memoryStream, recentFiles);

Ah. IDictionary not supported. MSDN says: "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."
In layman speak: "We ran out of time".

OK, an IDictionary is a with-frills List of KeyValuePairs - I will convert it and rebuild...

XmlSerializer does not complain about KeyValuePair items. Great! Only problem is that it stores everything except the actual and / or value information...

C#
KeyValuePair<string, string> kvp = new KeyValuePair<string, string>("My Key", "MyValue");
using (MemoryStream xml = new MemoryStream())
    {
    var serializer = new XmlSerializer(typeof(KeyValuePair<string, string>));
    serializer.Serialize(xml, kvp);
    xml.Seek(0, 0);
    byte[] bytes = xml.GetBuffer();
    string serialized = System.Text.Encoding.ASCII.GetString(bytes);
    }

Generates:
HTML
<KeyValuePairOfStringString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" />

So, a brute force and ignorance approach: create a new class, that can do it:
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.IO;
using System.Xml.Serialization;

namespace MyNamespace
    {
    /// <summary>
    /// Serialisable KeyValuePair
    /// </summary>
    [Serializable]
    public class SerialPair<K, V>
        {
        #region Properties
        /// <summary>
        /// Key
        /// </summary>
        public K Key { get; set; }
        /// <summary>
        /// Value
        /// </summary>
        public V Value { get; set; }
        #endregion
        #region Constructors
        /// <summary>
        /// Default constructor
        /// Required, or XmlSerializer complains.
        /// </summary>
        public SerialPair()
            {
            }
        /// <summary>
        /// Construct a SerialPair from a KeyValuePair of the same types.
        /// </summary>
        /// <param name="kvp"></param>
        public SerialPair(KeyValuePair<K, V> kvp)
            {
            Key = kvp.Key;
            Value = kvp.Value;
            }
        /// <summary>
        /// Construct a SerialPair from a Key and Value of the same types.
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        public SerialPair(K key, V value)
            {
            Key = key;
            Value = value;
            }
        /// <summary>
        /// Static constructor - allows as quick as possible detection of 
        /// non-serializable parameters.
        /// </summary>
        static SerialPair()
            {
            if (!typeof(K).IsSerializable && !(typeof(K) is ISerializable))
                {
                throw new InvalidOperationException("A serializable Type is required");
                }
            if (!typeof(V).IsSerializable && !(typeof(V) is ISerializable))
                {
                throw new InvalidOperationException("A serializable Type is required");
                }
            }
        #endregion
        #region Public Methods
        /// <summary>
        /// Regenerate a Dictionary from a serialised string
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        public static Dictionary<K, V> DeserializeDictionary(string s)
            {
            Dictionary<K, V> regeneratedTemplates;
            List<SerialPair<K, V>> list;
            byte[] combinedBytes = System.Text.Encoding.ASCII.GetBytes(s);
            using (MemoryStream sr = new MemoryStream(combinedBytes))
                {
                XmlSerializer deserializer = new XmlSerializer(typeof(List<SerialPair<K, V>>));
                list = (List<SerialPair<K, V>>)deserializer.Deserialize(sr);
                regeneratedTemplates = new Dictionary<K, V>();
                foreach (SerialPair<K, V> pair in list)
                    {
                    regeneratedTemplates.Add(pair.Key, pair.Value);
                    }
                }
            return regeneratedTemplates;
            }
        /// <summary>
        /// Generate a serialized string from a Dictionary.
        /// </summary>
        /// <param name="dict"></param>
        /// <returns></returns>
        public static string SerializeDictionary(Dictionary<K, V> dict)
            {
            string s;
            List<SerialPair<K, V>> list = new List<SerialPair<K, V>>(dict.Count);
            foreach (KeyValuePair<K, V> kvp in dict.ToArray())
                {
                SerialPair<K, V> sp = new SerialPair<K, V>(kvp);
                list.Add(sp);
                }
            using (MemoryStream xml = new MemoryStream())
                {
                XmlSerializer serializer = new XmlSerializer(typeof(List<SerialPair<K, V>>));
                serializer.Serialize(xml, list);
                xml.Seek(0, 0);
                byte[] bytes = xml.GetBuffer();
                s = System.Text.Encoding.ASCII.GetString(bytes);
                }
            return s;
            }
        #endregion
        }
    }


There is probably a much, much nicer way to do this (I could have used a collection that XmlSerializer does support, for example) but at least this way I won't forget that it doesn't work and try using it again...

[edit]Error in XML Comment to SerialPair(K, V) constructor caused compilation warning. Fixed - OriginalGriff[/edit]

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)