Foreground
I wanted to record some of the things I learned recently about XAML serialization of generic dictionaries. This subject is of interest to anybody who is interested in creating FrameworkElement objects that expose dictionary collections or those, like me, who are exploring the use of XAML serialization as an alternative for XML or other string-based serialization methods.
Background
A little background... My project at work is at the stage where its the last responsible moment to make architecture decisions about the designer. For us, that means using Visual Studio Shell to host our designer package (unless a deal breaker crops up).
So I examined ways that the design workflow would be handled, from currently executing server to the designer, in the editing environment, and into some kind of local repository (files on disk, source control, etc). Since Visual Studio is geared towards text file-based storage, I concentrated on the different ways I can get currently executing objects into some text format for editing and storage.
Originally, we had used a complicated system of interfaces and configuration objects that were designed for serialization to XML. It worked, and allowed us to control how things were serialized and deserialized, but there were some major issues. Not the least of which was the fact that since we weren't serializing objects but their configurations, our object graph relationships had to be stored as ids and assembly qualified names in generic collections. THAT meant that we were pushing dependencies on our serialization and deserialization objects deep into these object trees from the root object (which we controlled) to the leaves (which we planned to be add-ins written by people outside of our control). That made our API much more complicated and gave add-in writers much more control over the system than I wanted.
In addition to the add-in issues, since our references had to be stored as Guid/type generic collections, our configuration objects serialization was a pain in the ass. You cannot serialize dictionaries into XML. You have to implement IXmlSerializable and manually control serialization and deserialization.
Breakdown
And this is the point where the entire system breaks down. Pretty much every object that would be edited within the designer could, hypothetically, be an add-in, written by some doofus. They would be responsible for implementing our confusing configuration system correctly, which would mean having a configuration object that would be serializable to XML, which is hard enough without the fact that they would be forced to handle serializing dictionaries. IN ADDITION, it is VERY hard to manually control XML serialization in a generic way--i.e., so that element or property X can be found before or after Y or Z. I'd wager to say 30% of the coders out there would use StringBuilder and string.Split to handle XML serialization, and another 60% would use the correct XML DOM but would only be able to handle elements in an expected order. Considering I wanted designers to be able to edit these serialized objects directly via a text editor, this was simply unacceptable. And so comes XAML serialization.
XAML to the rescue
XAML is designed to serialize standard CLR object graphs. That means you can feed your average .NET object directly to the serializer with a minimum of muss or fuss. Visual Studio Core comes with the XAML editor, which includes great intellisense and validation support (as long as you provide an XSD). In addition, the simplicity of XAML serialization means that you can pretty much leave everything up to the serializer--as long as it passes validation, the XAML file will deserialize back into the original object. Almost.
The more you know...
There are some things you need to be aware of, but its much less of a hassle than XML serialization.
First off, your objects must have a parameterless constructor. This is pretty much standard for serialization work. So that means if you reference an object that needs to be serialized, and it doesn't include a parameterless constructor, and you don't control its code, you'll have to wrap it in a proxy object that does have a parameterless constructor.
Second, you can't serialize public fields via the XAML serializer. Only public properties will be serialized. Non-collection properties should have a public getter and a public setter. Collection properties should have a public getter, and should never return a null. These properties also need to be marked with a special attribute as well (see below).
Third, serialization can be controlled declaratively via the DesignerSerializationVisibility attribute. This attribute takes an enum that indicates the property is either Visible, Content or Hidden. Obviously, if you don't want your property serialized you would mark it as Hidden or make its setter private. Collection properties (which should not have public setters) should be marked as Content, because public properties without setters are treated as Hidden. The default for any property without this attribute is Visible.
Fourth, generics are supported, but generic dictionaries still suck. There are two ways to get around this. First, you can create a serializer that extends CodeDomSerializer and instruct the XAML serializer to use this for the generic dictionary by marking it with the DesignerSerializer attribute. Good luck if you want to do this; there isn't any documentation on it that's worth a damn. An easier way to handle generic dictionaries is to create a wrapper that extends KeyedCollection. KeyedCollection is a specialized type of dictionary that the XAML serializer can handle easily, yet still provides some of the benefits of generics.
Examples
Here's a serializable object with a standard property of type string, and a couple collections. The generic dictionary collection Fail won't deserialize property, whereas the KeyedCollection wrapped by MyGenericDictionary serializes just fine.
public class SerializedObjectLol
{
public string Name { get; set; }
private Dictionary<Guid, Type> _dict;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public Dictionary<Guid, Type> Fail { get { return _dict ?? (_dict = new Dictionary<Guid, Type>()); } }
private MyGenericDictionary _dictionary;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public KeyedCollection<Guid, GuidTypePair> MyDictionary { get { return _dictionary ?? (_dictionary = new MyGenericDictionary()); } }
}
To wrap a KeyedCollection, you have to do two things. First, you have to provide a class that wraps your key/value pair, providing public getters and setters for their properties (the generic KeyValuePair is immutable, so you can't use it). KeyedCollection is abstract, so you'll have to override a single method, GetKeyForItem. This method asks you to provide the base collection with the key for the type being stored in the collection and is simple to implement.
public class MyGenericDictionary : KeyedCollection<Guid, GuidTypePair>
{
protected override Guid GetKeyForItem(GuidTypePair item)
{
return item.Key;
}
}
public class GuidTypePair
{
public Guid Key { get; set; }
public Type Value { get; set; }
}
Notice the type arguments provided the base collection. The first type is the type of the key; the second type is the type of the object that contains both the key and the value, not the type of the value.
Here's the code for serialization and deserialization (performed in memory):
/// <summary>
/// Serializes the specified object
/// </summary>
/// <param name="toSerialize">Object to serialize.</param>
/// <returns>The object serialized to XAML</returns>
private string Serialize(object toSerialize)
{
XmlWriterSettings settings = new XmlWriterSettings();
// You might want to wrap these in #if DEBUG's
settings.Indent = true;
settings.NewLineOnAttributes = true;
// this gets rid of the XML version
settings.ConformanceLevel = ConformanceLevel.Fragment;
// buffer to a stringbuilder
StringBuilder sb = new StringBuilder();
XmlWriter writer = XmlWriter.Create(sb, settings);
// Need moar documentation on the manager, plox MSDN
XamlDesignerSerializationManager manager = new XamlDesignerSerializationManager(writer);
manager.XamlWriterMode = XamlWriterMode.Expression;
// its extremely rare for this to throw an exception
XamlWriter.Save(toSerialize, manager);
return sb.ToString();
}
/// <summary>
/// Deserializes an object from xaml.
/// </summary>
/// <param name="xamlText">The xaml text.</param>
/// <returns>The deserialized object</returns>
/// <exception cref="XmlException">Thrown if the serialized text is not well formed XML</exception>
/// <exception cref="XamlParseException">Thrown if unable to deserialize from xaml</exception>
private object Deserialize(string xamlText)
{
XmlDocument doc = new XmlDocument();
// may throw XmlException
doc.LoadXml(xamlText);
// may throw XamlParseException
return XamlReader.Load(new XmlNodeReader(doc));
}
The serialize method is a bit of overkill; you can typically use the override for XamlWriter.Save that takes an object and returns a string. The version I provided tabifys the result so its nicer to read. An interesting side note is the XamlDesignerSerializationManager, which has methods to add and get "service providers" for use, I assume, during serialization. There is no documentation available about this manager or how these providers are used. I'd guess that if you mark a property with the DesignerSerializerAttribute, this is where you'd find them.
TL;DR
XAML serialization definitely beats XML for working with object graphs in a text format. It works on public properties only, and requires you to have a parameterless constructor. You can control how properties are serialized using the DesignerSerializationVisibility and DesignerSerializer attributes. And while the XAML serializer doesn't freak out about generics, you still have to abandon your generic dictionaries for collections that extend KeyedCollection.
UPDATE!
I'd like to say "Hi!" to the guys at Microsoft reading this. Yes, I know you're reading this; you sent this blog post, copied and pasted into a word document, to me in response to some questions about XAML serialization. Look, if you want me to do your work, that's great. My rates are reasonable. Just ask me next time and I'll even edit out the "sucks" for you!