Wednesday, October 19, 2005

Serializing a KeyedCollection

The other day I pointed out System.Collections.ObjectModel.KeyedCollection. Towards the end, I claimed that you could use XmlSerializer to serialize an instance. Well, it turns out this is only true if you use System.String as the key type. For some reason, the serializer barfs with "The given key was not present in the dictionary" when you try to serialize a collection that's keyed off of any other type. Or at least, any of the other types I tried.

 

I'm not sure if this is a serializer bug, an issue with KeyedCollection, or if it's a by-design limitation for some subtle reason. I know I'd sure like it if it started working in the RTM version of Whidbey.

 

Update: Astute readers Aaron and Bart figured out that this is due to a bug in XmlSerializer (a bug which made #1 on the MSDN Product Feedback Center). Apparently XmlSerializer calls the object IList.this[int index] indexer, rather than the type-specific indexer. Fortunately, there's an easy workaround: just add an new indexer which hides the indexer in the base class. Modifying my earlier example, you'd do this:

 

class People : KeyedCollection<string, Person> {

   protected override string GetKeyForItem(Person item) {

      return item.Name;

   } 

 

   public new Person this[int index] {

      get { return ((IList<Person>)this)[index];

   }

}

 

I've put the new code in bold. Of course, it already works when string is the key type, but you get the idea. The key here is the "new" keyword on the method, which makes this indexer hide the indexer in the base class, causing it to be used instead.

 

I've included a more complete sample below that uses a DateTime (birthday) as the key type instead of a string. Note that (as far as I can figure so far) you can't use an int as the key type, because that conflicts with the positional indexer already exposed by IList<T>. And it makes sense - if I were representing age as an integer, and my age were 33, how would I know whether people[33] meant "the 33rd person" or "the person with age 33"?

 

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

namespace SerializeKeyedCollection {
    class Program {
        static void Main(string[] args) {
            // Set up a collection with two people in it
            People people = new People();

            Person craig = new Person();
            craig.Name = "Craig";
            craig.Birthday = new DateTime(1971, 11, 29);

            people.Add(craig);

            Person alice = new Person();
            alice.Name = "Alice";
            alice.Birthday = new DateTime(1973, 11, 3);

            people.Add(alice);

            // Serialize the collection to a string
            StringBuilder stringBuilder = new StringBuilder();
            StringWriter stringWriter = new StringWriter(stringBuilder); 
            XmlSerializer ser = new XmlSerializer(typeof(People));
            ser.Serialize(stringWriter, people);

            // Print out the serialized collection
            string xml = stringBuilder.ToString();
            Console.WriteLine(xml);

            // Deserialize the collection to show that deserialization works
            StringReader stringReader = new StringReader(xml);
            people = (People) ser.Deserialize(stringReader);

            DateTime birthday = new DateTime(1971, 11, 29);
            Console.WriteLine("Person with key 11/29/1971 is {0}", people[birthday].Name); 
        }
    }

    public class Person {
        private DateTime _birthday;
        private string _name;

        public DateTime Birthday  {
            get { return _birthday; }
            set { _birthday = value; }
        }

        public string Name {
            get { return _name; }
            set { _name = value; }
        }
    }

    public class People : KeyedCollection<DateTime, Person> {
        protected override DateTime GetKeyForItem(Person item) {
            return item.Birthday; 
        }

        public new Person this[int index] {
            get { return ((IList<Person>)this)[index]; }
        }
    }
}


 

9 comments:

  1. Craig,



    Thanks for blogging about this - wish I'd spotted it sooner, it would have saved me much time and stress this morning!



    Regards

    Ian

    ReplyDelete
  2. We aim to please. ;)

    ReplyDelete
  3. It's over a month now since Visual Studio 2005 officially RTM'd, and during that time I've been fortunate...

    ReplyDelete
  4. is there an easy way to serialize *only* the keys in a KeyedCollection?

    ReplyDelete
  5. You could serialize KeyedCollection.Dictionary.Keys.

    ReplyDelete
  6. It's over a month now since Visual Studio 2005 officially RTM'd, and during that time I've been fortunate

    ReplyDelete
  7. At least in VB, an integer key works fine. But a damned f. bug makes serialization fail if no item has a Key of 0.

    Amazing !

    ReplyDelete
  8. I'm trying to serialize a keyedcollection of objects to XML.
    It has a string key.
    Serializer does not throw any exceptions but it does not serialise the properties of object.
    So, if I refer to example above, xml output is-

    ReplyDelete
  9. Thanks for the clever workaround. This is exactly the problem I ran into, and your solution worked beautifully.

    ReplyDelete